feat(Deployment): Integrate Ansible deployment via PHP deployment pipeline

- Create AnsibleDeployStage using framework's Process module for secure command execution
- Integrate AnsibleDeployStage into DeploymentPipelineCommands for production deployments
- Add force_deploy flag support in Ansible playbook to override stale locks
- Use PHP deployment module as orchestrator (php console.php deploy:production)
- Fix ErrorAggregationInitializer to use Environment class instead of $_ENV superglobal

Architecture:
- BuildStage → AnsibleDeployStage → HealthCheckStage for production
- Process module provides timeout, error handling, and output capture
- Ansible playbook supports rollback via rollback-git-based.yml
- Zero-downtime deployments with health checks
This commit is contained in:
2025-10-26 14:08:07 +01:00
parent a90263d3be
commit 3b623e7afb
170 changed files with 19888 additions and 575 deletions

View File

@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\MachineLearning\Events;
use App\Framework\Queue\MachineLearning\ValueObjects\JobAnomalyResult;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Queue\ValueObjects\JobMetrics;
/**
* Queue Job Anomaly Detected Event
*
* Dispatched when the anomaly monitor detects unusual job behavior.
*
* This event can be used to:
* - Send notifications to monitoring systems
* - Trigger automated remediation workflows
* - Update dashboards and metrics
* - Store anomaly results for analysis
*/
final readonly class QueueJobAnomalyDetectedEvent
{
public function __construct(
public JobMetrics $metrics,
public JobMetadata $metadata,
public JobAnomalyResult $anomalyResult
) {}
/**
* Get event type identifier
*/
public function getEventType(): string
{
return 'queue.job.anomaly_detected';
}
/**
* Get severity level
*/
public function getSeverity(): string
{
return $this->anomalyResult->getSeverity();
}
/**
* Check if this is a critical anomaly
*/
public function isCritical(): bool
{
return $this->anomalyResult->requiresImmediateAttention();
}
/**
* Get event payload for logging/notification
*/
public function toArray(): array
{
return [
'event_type' => $this->getEventType(),
'job_id' => $this->metrics->jobId,
'queue_name' => $this->metrics->queueName,
'job_class' => $this->metadata->class->toString(),
'anomaly_score' => $this->anomalyResult->anomalyScore->value(),
'severity' => $this->getSeverity(),
'is_critical' => $this->isCritical(),
'primary_indicator' => $this->anomalyResult->primaryIndicator,
'detected_patterns' => array_map(
fn($pattern) => [
'type' => $pattern['type'],
'confidence' => $pattern['confidence']->value(),
'description' => $pattern['description']
],
$this->anomalyResult->detectedPatterns
),
'recommended_action' => $this->anomalyResult->getRecommendedAction(),
'job_metrics' => [
'execution_time_ms' => $this->metrics->executionTimeMs,
'memory_usage_mb' => $this->metrics->getMemoryUsageMB(),
'attempts' => $this->metrics->attempts,
'status' => $this->metrics->status
],
'timestamp' => date('Y-m-d H:i:s')
];
}
}

View File

@@ -55,14 +55,14 @@ final readonly class JobAnomalyDetector
$overallScore = $this->calculateOverallScore($featureScores, $detectedPatterns);
// Step 4: Determine if anomalous based on threshold
$isAnomalous = $overallScore->getValue() >= $this->anomalyThreshold->getValue();
$isAnomalous = $overallScore->value() >= $this->anomalyThreshold->value();
// Step 5: Identify primary indicator (highest scoring feature)
$primaryIndicator = $this->identifyPrimaryIndicator($featureScores);
// Step 6: Build result
if (!$isAnomalous) {
if ($overallScore->getValue() > 0) {
if ($overallScore->value() > 0) {
return JobAnomalyResult::lowConfidence(
$overallScore,
$featureScores,
@@ -97,7 +97,7 @@ final readonly class JobAnomalyDetector
foreach ($featureArray as $featureName => $value) {
// Convert feature value (0.0-1.0) to anomaly score
$anomalyScore = $this->featureValueToAnomalyScore($featureName, $value);
$scores[$featureName] = Score::fromDecimal($anomalyScore);
$scores[$featureName] = new Score($anomalyScore);
}
return $scores;
@@ -174,7 +174,7 @@ final readonly class JobAnomalyDetector
$patterns[] = [
'type' => 'high_failure_risk',
'confidence' => Score::fromDecimal($confidence),
'confidence' => new Score($confidence),
'description' => sprintf(
'High failure rate (%.1f%%) with excessive retries (%.1f%%)',
$features->failureRate * 100,
@@ -192,7 +192,7 @@ final readonly class JobAnomalyDetector
$patterns[] = [
'type' => 'performance_degradation',
'confidence' => Score::fromDecimal($confidence),
'confidence' => new Score($confidence),
'description' => sprintf(
'Unstable execution times (variance: %.1f%%) and memory patterns (%.1f%%)',
$features->executionTimeVariance * 100,
@@ -210,7 +210,7 @@ final readonly class JobAnomalyDetector
$patterns[] = [
'type' => 'resource_exhaustion',
'confidence' => Score::fromDecimal($confidence),
'confidence' => new Score($confidence),
'description' => sprintf(
'High queue depth impact (%.1f%%) with memory anomalies (%.1f%%)',
$features->queueDepthCorrelation * 100,
@@ -228,7 +228,7 @@ final readonly class JobAnomalyDetector
$patterns[] = [
'type' => 'automated_execution',
'confidence' => Score::fromDecimal($confidence),
'confidence' => new Score($confidence),
'description' => sprintf(
'Very regular timing (%.1f%%) with low variance (%.1f%%) - possible bot activity',
$features->executionTimingRegularity * 100,
@@ -246,7 +246,7 @@ final readonly class JobAnomalyDetector
$patterns[] = [
'type' => 'data_processing_anomaly',
'confidence' => Score::fromDecimal($confidence),
'confidence' => new Score($confidence),
'description' => sprintf(
'Unusual payload sizes (%.1f%%) with memory pattern anomalies (%.1f%%)',
$features->payloadSizeAnomaly * 100,
@@ -311,7 +311,7 @@ final readonly class JobAnomalyDetector
foreach ($featureScores as $featureName => $score) {
$weight = $weights[$featureName] ?? 1.0;
$weightedSum += $score->getValue() * $weight;
$weightedSum += $score->value() * $weight;
$totalWeight += $weight;
}
@@ -320,10 +320,10 @@ final readonly class JobAnomalyDetector
// Pattern-based boosting
$patternBoost = $this->calculatePatternBoost($detectedPatterns);
// Combine base score and pattern boost (max 100%)
$finalScore = min(100.0, $baseScore + $patternBoost);
// Combine base score and pattern boost (max 1.0)
$finalScore = min(1.0, $baseScore + $patternBoost);
return new Score((int) round($finalScore));
return new Score($finalScore);
}
/**
@@ -341,7 +341,7 @@ final readonly class JobAnomalyDetector
$boost = 0.0;
foreach ($detectedPatterns as $pattern) {
$confidence = $pattern['confidence']->getValue();
$confidence = $pattern['confidence']->value();
if ($confidence >= 70) {
$boost += 10.0; // High confidence pattern: +10%
@@ -369,8 +369,8 @@ final readonly class JobAnomalyDetector
$primaryIndicator = 'unknown';
foreach ($featureScores as $featureName => $score) {
if ($score->getValue() > $maxScore) {
$maxScore = $score->getValue();
if ($score->value() > $maxScore) {
$maxScore = $score->value();
$primaryIndicator = $featureName;
}
}
@@ -384,9 +384,19 @@ final readonly class JobAnomalyDetector
public function getConfiguration(): array
{
return [
'anomaly_threshold' => $this->anomalyThreshold->getValue(),
'anomaly_threshold' => $this->anomalyThreshold->value(),
'z_score_threshold' => $this->zScoreThreshold,
'iqr_multiplier' => $this->iqrMultiplier
];
}
/**
* Get the anomaly threshold
*
* @return Score Minimum score to classify as anomalous
*/
public function getThreshold(): Score
{
return $this->anomalyThreshold;
}
}

View File

@@ -89,7 +89,7 @@ final readonly class QueueAnomalyModelAdapter
// Determine prediction
$prediction = $analysisResult->isAnomalous;
$confidence = $analysisResult->anomalyScore->getValue() / 100.0; // Convert 0-100 to 0.0-1.0
$confidence = $analysisResult->anomalyScore->value(); // Already 0.0-1.0 range
// Track prediction in performance monitor
$this->performanceMonitor->trackPrediction(
@@ -104,9 +104,9 @@ final readonly class QueueAnomalyModelAdapter
// Convert result to array format
$resultArray = [
'is_anomalous' => $analysisResult->isAnomalous,
'anomaly_score' => $analysisResult->anomalyScore->getValue(),
'anomaly_score' => $analysisResult->anomalyScore->value(),
'feature_scores' => array_map(
fn($score) => $score->getValue(),
fn($score) => $score->value(),
$analysisResult->featureScores
),
'detected_patterns' => $analysisResult->detectedPatterns,

View File

@@ -0,0 +1,216 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\MachineLearning;
use App\Framework\Queue\MachineLearning\ValueObjects\JobAnomalyResult;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Queue\ValueObjects\JobMetrics;
use App\Framework\Logging\Logger;
use App\Framework\Events\EventDispatcher;
/**
* Queue Anomaly Monitor
*
* Monitors queue job execution for anomalies using ML-based detection.
*
* Features:
* - Real-time anomaly detection on job completion
* - Automatic alerting for critical anomalies
* - Anomaly result persistence
* - Event dispatching for anomaly notifications
*/
final readonly class QueueAnomalyMonitor
{
public function __construct(
private JobAnomalyDetector $anomalyDetector,
private QueueJobFeatureExtractor $featureExtractor,
private JobMetricsManager $metricsManager,
private Logger $logger,
private ?EventDispatcher $eventDispatcher = null
) {}
/**
* Analyze job for anomalies after execution
*
* @param JobMetrics $metrics Job execution metrics
* @param JobMetadata $metadata Job metadata
* @param int $queueDepth Current queue size
* @return JobAnomalyResult Detection result
*/
public function analyzeJobExecution(
JobMetrics $metrics,
JobMetadata $metadata,
int $queueDepth = 0
): JobAnomalyResult {
// Extract features from job metrics
$features = $this->featureExtractor->extractFeatures($metrics, $metadata, $queueDepth);
// Detect anomalies
$result = $this->anomalyDetector->detect($features);
// Log anomaly if detected
if ($result->isAnomalous) {
$this->logAnomaly($metrics, $metadata, $result);
}
// Alert on critical anomalies
if ($result->requiresImmediateAttention()) {
$this->alertCriticalAnomaly($metrics, $metadata, $result);
}
// Dispatch event for anomaly notification
if ($result->isAnomalous && $this->eventDispatcher !== null) {
$this->eventDispatcher->dispatch(
new QueueJobAnomalyDetectedEvent($metrics, $metadata, $result)
);
}
return $result;
}
/**
* Analyze multiple jobs in batch
*
* @param array $jobsData Array of [JobMetrics, JobMetadata] tuples
* @return array Array of JobAnomalyResult indexed by job ID
*/
public function analyzeBatch(array $jobsData): array
{
$results = [];
foreach ($jobsData as [$metrics, $metadata]) {
$results[$metrics->jobId] = $this->analyzeJobExecution($metrics, $metadata);
}
return $results;
}
/**
* Get anomaly statistics for a queue
*
* @param string $queueName Queue to analyze
* @param string $timeWindow Time window (e.g., '24 hours')
* @return array Anomaly statistics
*/
public function getQueueAnomalyStats(string $queueName, string $timeWindow = '24 hours'): array
{
// This would query persisted anomaly results
// For now, return placeholder stats
return [
'queue_name' => $queueName,
'time_window' => $timeWindow,
'total_jobs_analyzed' => 0,
'anomalies_detected' => 0,
'critical_anomalies' => 0,
'anomaly_rate' => 0.0,
'most_common_patterns' => [],
'top_anomalous_jobs' => []
];
}
/**
* Log anomaly detection
*/
private function logAnomaly(
JobMetrics $metrics,
JobMetadata $metadata,
JobAnomalyResult $result
): void {
$this->logger->warning('Queue job anomaly detected', [
'job_id' => $metrics->jobId,
'queue_name' => $metrics->queueName,
'job_class' => $metadata->class->toString(),
'anomaly_score' => $result->anomalyScore->value(),
'severity' => $result->getSeverity(),
'primary_indicator' => $result->primaryIndicator,
'detected_patterns' => array_map(
fn($pattern) => [
'type' => $pattern['type'],
'confidence' => $pattern['confidence']->value()
],
$result->detectedPatterns
),
'execution_time_ms' => $metrics->executionTimeMs,
'memory_usage_mb' => $metrics->getMemoryUsageMB(),
'attempts' => $metrics->attempts,
'status' => $metrics->status
]);
}
/**
* Alert on critical anomalies
*/
private function alertCriticalAnomaly(
JobMetrics $metrics,
JobMetadata $metadata,
JobAnomalyResult $result
): void {
$this->logger->critical('CRITICAL queue job anomaly requires immediate attention', [
'job_id' => $metrics->jobId,
'queue_name' => $metrics->queueName,
'job_class' => $metadata->class->toString(),
'anomaly_score' => $result->anomalyScore->value(),
'severity' => $result->getSeverity(),
'recommended_action' => $result->getRecommendedAction(),
'top_contributors' => $result->getTopContributors(3),
'alert_type' => 'queue_job_critical_anomaly',
'alert_priority' => 'high'
]);
// In a full implementation, this would:
// - Send PagerDuty/OpsGenie alert
// - Post to Slack channel
// - Trigger incident response workflow
}
/**
* Enable anomaly monitoring for a queue
*
* This would be called during queue worker initialization.
*
* @param string $queueName Queue to monitor
*/
public function enableMonitoring(string $queueName): void
{
$this->logger->info('Anomaly monitoring enabled', [
'queue_name' => $queueName,
'detector_threshold' => $this->anomalyDetector->getThreshold()->value(),
'monitoring_active' => true
]);
}
/**
* Disable anomaly monitoring for a queue
*
* @param string $queueName Queue to stop monitoring
*/
public function disableMonitoring(string $queueName): void
{
$this->logger->info('Anomaly monitoring disabled', [
'queue_name' => $queueName,
'monitoring_active' => false
]);
}
/**
* Get monitoring status
*
* @return array Monitoring configuration and stats
*/
public function getMonitoringStatus(): array
{
return [
'enabled' => true,
'detector_threshold' => $this->anomalyDetector->getThreshold()->value(),
'z_score_threshold' => 3.0, // From detector config
'iqr_multiplier' => 1.5, // From detector config
'monitored_queues' => [], // Would list active queues
'detection_count_24h' => 0, // Would query persisted results
'critical_count_24h' => 0
];
}
}

View File

@@ -0,0 +1,252 @@
<?php
declare(strict_types=1);
namespace App\Framework\Queue\MachineLearning;
use App\Framework\Queue\MachineLearning\ValueObjects\JobFeatures;
use App\Framework\Queue\Services\JobMetricsManager;
use App\Framework\Queue\ValueObjects\JobMetadata;
use App\Framework\Queue\ValueObjects\JobMetrics;
/**
* Queue Job Feature Extractor
*
* Extracts normalized features from queue job metrics for anomaly detection.
*
* Features extracted:
* - execution_time_variance: Variance from average execution time for job type
* - memory_usage_pattern: Deviation from typical memory usage
* - retry_frequency: Normalized retry count
* - failure_rate: Failure ratio for job type
* - queue_depth_correlation: Relationship between execution time and queue size
* - dependency_chain_complexity: Number of dependent jobs
* - payload_size_anomaly: Deviation from typical payload size
* - execution_timing_regularity: Consistency of execution intervals
*/
final readonly class QueueJobFeatureExtractor
{
public function __construct(
private JobMetricsManager $metricsManager
) {}
/**
* Extract JobFeatures from job execution metrics
*
* @param JobMetrics $currentMetrics Current job metrics
* @param JobMetadata $metadata Job metadata
* @param int $queueDepth Current queue size
* @return JobFeatures Normalized feature vector (all values 0.0-1.0)
*/
public function extractFeatures(
JobMetrics $currentMetrics,
JobMetadata $metadata,
int $queueDepth = 0
): JobFeatures {
// Get historical stats for this job type
$historicalStats = $this->metricsManager->getPerformanceStats(
queueName: $currentMetrics->queueName,
timeWindow: '24 hours'
);
return new JobFeatures(
executionTimeVariance: $this->calculateExecutionTimeVariance($currentMetrics, $historicalStats),
memoryUsagePattern: $this->calculateMemoryUsagePattern($currentMetrics, $historicalStats),
retryFrequency: $this->calculateRetryFrequency($currentMetrics),
failureRate: $this->calculateFailureRate($historicalStats),
queueDepthCorrelation: $this->calculateQueueDepthCorrelation($queueDepth),
dependencyChainComplexity: $this->calculateDependencyComplexity($metadata),
payloadSizeAnomaly: $this->calculatePayloadSizeAnomaly($metadata, $historicalStats),
executionTimingRegularity: $this->calculateExecutionTimingRegularity($currentMetrics)
);
}
/**
* Calculate execution time variance (0.0-1.0)
*
* Measures how much the current execution time deviates from the average.
* High variance indicates unstable performance.
*/
private function calculateExecutionTimeVariance(
JobMetrics $metrics,
array $historicalStats
): float {
$avgExecutionTime = $historicalStats['average_execution_time_ms'] ?? 0;
if ($avgExecutionTime <= 0) {
return 0.0; // No historical data yet
}
$currentExecutionTime = $metrics->executionTimeMs;
$deviation = abs($currentExecutionTime - $avgExecutionTime) / $avgExecutionTime;
// Normalize: 0 = exactly average, 1.0 = 10x or more deviation
return min(1.0, $deviation / 10.0);
}
/**
* Calculate memory usage pattern (0.0-1.0)
*
* Measures memory usage anomaly compared to historical average.
*/
private function calculateMemoryUsagePattern(
JobMetrics $metrics,
array $historicalStats
): float {
$avgMemoryUsage = $historicalStats['average_memory_usage_bytes'] ?? 0;
if ($avgMemoryUsage <= 0) {
return 0.0; // No historical data yet
}
$currentMemoryUsage = $metrics->memoryUsageBytes;
$deviation = abs($currentMemoryUsage - $avgMemoryUsage) / $avgMemoryUsage;
// Normalize: 0 = average usage, 1.0 = 5x or more deviation
return min(1.0, $deviation / 5.0);
}
/**
* Calculate retry frequency (0.0-1.0)
*
* Normalized retry count: 0 = no retries, 1.0 = max attempts exhausted
*/
private function calculateRetryFrequency(JobMetrics $metrics): float
{
if ($metrics->maxAttempts <= 1) {
return 0.0; // No retry configuration
}
return min(1.0, $metrics->attempts / $metrics->maxAttempts);
}
/**
* Calculate failure rate (0.0-1.0)
*
* Percentage of failed jobs for this queue over time window.
*/
private function calculateFailureRate(array $historicalStats): float
{
$totalJobs = $historicalStats['total_jobs'] ?? 0;
if ($totalJobs === 0) {
return 0.0;
}
$failedJobs = $historicalStats['failed_jobs'] ?? 0;
return min(1.0, $failedJobs / $totalJobs);
}
/**
* Calculate queue depth correlation (0.0-1.0)
*
* Impact of queue depth on performance.
* High values indicate system is overloaded.
*/
private function calculateQueueDepthCorrelation(int $queueDepth): float
{
// Normalize queue depth: 0 = empty, 1.0 = 1000+ jobs queued
return min(1.0, $queueDepth / 1000.0);
}
/**
* Calculate dependency chain complexity (0.0-1.0)
*
* Currently a placeholder - would analyze job dependency graph.
* For now, use job tags to estimate complexity.
*/
private function calculateDependencyComplexity(JobMetadata $metadata): float
{
$tagCount = count($metadata->tags);
// Simple heuristic: more tags = more complex job
return min(1.0, $tagCount / 10.0);
}
/**
* Calculate payload size anomaly (0.0-1.0)
*
* Deviation from typical payload size for this job type.
* Currently estimates from metadata extra fields.
*/
private function calculatePayloadSizeAnomaly(
JobMetadata $metadata,
array $historicalStats
): float {
$extraFieldCount = count($metadata->extra);
// Simple heuristic: more extra fields = larger payload
// Normalize: 0 = typical, 1.0 = 50+ extra fields
return min(1.0, $extraFieldCount / 50.0);
}
/**
* Calculate execution timing regularity (0.0-1.0)
*
* Measures consistency of execution intervals.
* High regularity (near 1.0) can indicate bot-like behavior.
*/
private function calculateExecutionTimingRegularity(JobMetrics $metrics): float
{
// For now, use job type consistency as proxy
// In a full implementation, would analyze inter-arrival times
// If job has metadata indicating scheduled execution, mark as regular
$metadata = $metrics->metadata ?? [];
if (isset($metadata['scheduled']) && $metadata['scheduled']) {
return 0.9; // Scheduled jobs are highly regular (expected)
}
// Default: moderate regularity for queue jobs
return 0.3;
}
/**
* Extract features from job metrics history for batch analysis
*
* @param string $jobId Job ID to analyze
* @return JobFeatures[] Array of feature vectors over time
*/
public function extractHistoricalFeatures(string $jobId): array
{
$metricsHistory = $this->metricsManager->getJobMetricsHistory($jobId);
$features = [];
foreach ($metricsHistory as $metrics) {
// Create minimal metadata from metrics
$metadata = new JobMetadata(
id: new \App\Framework\Ulid\Ulid(new \App\Framework\DateTime\SystemClock()),
class: \App\Framework\Core\ValueObjects\ClassName::create($metrics->queueName),
type: 'job',
queuedAt: \App\Framework\Core\ValueObjects\Timestamp::now(),
tags: [],
extra: $metrics->metadata ?? []
);
$features[] = $this->extractFeatures($metrics, $metadata, 0);
}
return $features;
}
/**
* Extract features for all recent jobs in a queue
*
* @param string $queueName Queue to analyze
* @param int $limit Maximum number of jobs to analyze
* @return array Array of [JobMetrics, JobFeatures] tuples
*/
public function extractQueueFeatures(string $queueName, int $limit = 100): array
{
// Get recent job metrics for this queue
$historicalStats = $this->metricsManager->getPerformanceStats($queueName, '1 hour');
// This would need a method to get recent jobs - placeholder for now
// In a full implementation, would query job_metrics table for recent jobs
return [];
}
}

View File

@@ -118,9 +118,9 @@ final readonly class JobAnomalyResult
}
return match (true) {
$this->anomalyScore->getValue() >= 80 => 'critical',
$this->anomalyScore->getValue() >= 60 => 'high',
$this->anomalyScore->getValue() >= 40 => 'medium',
$this->anomalyScore->value() >= 80 => 'critical',
$this->anomalyScore->value() >= 60 => 'high',
$this->anomalyScore->value() >= 40 => 'medium',
default => 'low'
};
}
@@ -205,7 +205,7 @@ final readonly class JobAnomalyResult
// Calculate total score for percentage calculation
$totalScore = array_reduce(
$this->featureScores,
fn(float $carry, Score $score) => $carry + $score->getValue(),
fn(float $carry, Score $score) => $carry + $score->value(),
0.0
);
@@ -215,7 +215,7 @@ final readonly class JobAnomalyResult
// Sort features by score descending
$sorted = $this->featureScores;
uasort($sorted, fn(Score $a, Score $b) => $b->getValue() <=> $a->getValue());
uasort($sorted, fn(Score $a, Score $b) => $b->value() <=> $a->value());
// Take top N and calculate contribution percentages
$contributors = [];
@@ -229,7 +229,7 @@ final readonly class JobAnomalyResult
$contributors[] = [
'feature' => $feature,
'score' => $score,
'contribution_percentage' => ($score->getValue() / $totalScore) * 100.0
'contribution_percentage' => ($score->value() / $totalScore) * 100.0
];
$count++;
@@ -294,7 +294,7 @@ final readonly class JobAnomalyResult
*/
public function getConfidenceLevel(): string
{
$value = $this->anomalyScore->getValue();
$value = $this->anomalyScore->value();
return match (true) {
$value >= 80 => 'very_high',
@@ -313,7 +313,7 @@ final readonly class JobAnomalyResult
public function toArray(): array
{
return [
'anomaly_score' => $this->anomalyScore->getValue(),
'anomaly_score' => $this->anomalyScore->value(),
'is_anomalous' => $this->isAnomalous,
'severity' => $this->getSeverity(),
'confidence_level' => $this->getConfidenceLevel(),
@@ -321,13 +321,13 @@ final readonly class JobAnomalyResult
'detected_patterns' => array_map(
fn(array $pattern) => [
'type' => $pattern['type'],
'confidence' => $pattern['confidence']->getValue(),
'confidence' => $pattern['confidence']->value(),
'description' => $pattern['description']
],
$this->detectedPatterns
),
'feature_scores' => array_map(
fn(Score $score) => $score->getValue(),
fn(Score $score) => $score->value(),
$this->featureScores
),
'top_contributors' => $this->getTopContributors(3),
@@ -346,7 +346,7 @@ final readonly class JobAnomalyResult
return 'JobAnomalyResult[Normal]';
}
$score = $this->anomalyScore->getValue();
$score = $this->anomalyScore->value();
$severity = $this->getSeverity();
$patterns = implode(', ', $this->getPatternTypes());