TenantAtlas/apps/platform/app/Support/Baselines/CompareSemantics/BaselineCompareRunSummaryClassifier.php
ahmido ea77c8c718 feat(baselines): implement baseline compare result semantics (#454)
Implemented deterministic Baseline Result Semantics (Spec 383), introducing CompareSubjectResult and CompareEvidenceResult. Replaced generic arrays with strict Data Transfer Objects for Baseline engine output.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #454
2026-06-16 20:20:27 +00:00

136 lines
4.4 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Baselines\CompareSemantics;
use App\Support\OperationRunOutcome;
final class BaselineCompareRunSummaryClassifier
{
/**
* @param list<CompareSubjectOutcome|array<string, mixed>> $subjectOutcomes
* @param list<string> $uncoveredTypes
* @return array{
* run_outcome: string,
* operation_outcome: string,
* counts: array<string, array<string, int>>
* }
*/
public function summarize(
array $subjectOutcomes,
int $driftFindingsCount,
bool $warningsRecorded,
bool $resumeTokenPresent = false,
array $uncoveredTypes = [],
): array {
$counts = [
'by_reason' => [],
'by_category' => [],
'by_actionability' => [],
'by_readiness_impact' => [],
];
$verifiedSubjectCount = 0;
$blockingSubjectCount = 0;
$failedSubjectCount = 0;
foreach ($subjectOutcomes as $outcome) {
$outcome = $outcome instanceof CompareSubjectOutcome ? $outcome->toArray() : $outcome;
if (! is_array($outcome)) {
continue;
}
$reason = $this->nonEmptyString($outcome['reason'] ?? null);
$category = $this->nonEmptyString($outcome['category'] ?? null);
$actionability = $this->nonEmptyString($outcome['actionability'] ?? null);
$readinessImpact = $this->nonEmptyString($outcome['readiness_impact'] ?? null);
$this->increment($counts['by_reason'], $reason);
$this->increment($counts['by_category'], $category);
$this->increment($counts['by_actionability'], $actionability);
$this->increment($counts['by_readiness_impact'], $readinessImpact);
if (in_array($category, [CompareResultCategory::Verified->value, CompareResultCategory::DriftDetected->value], true)) {
$verifiedSubjectCount++;
}
if (in_array($readinessImpact, [CompareResultReadinessImpact::CustomerBlocker->value, CompareResultReadinessImpact::InternalBlocker->value], true)) {
$blockingSubjectCount++;
}
if ($category === CompareResultCategory::Failed->value) {
$failedSubjectCount++;
}
}
foreach ($counts as &$bucket) {
ksort($bucket);
}
unset($bucket);
$hasWarnings = $warningsRecorded || $resumeTokenPresent || $uncoveredTypes !== [];
$runOutcome = $this->runOutcome(
driftFindingsCount: $driftFindingsCount,
verifiedSubjectCount: $verifiedSubjectCount,
blockingSubjectCount: $blockingSubjectCount,
failedSubjectCount: $failedSubjectCount,
hasWarnings: $hasWarnings,
);
return [
'run_outcome' => $runOutcome->value,
'operation_outcome' => in_array($runOutcome, [CompareRunOutcome::Completed, CompareRunOutcome::CompletedWithDrift], true)
? OperationRunOutcome::Succeeded->value
: OperationRunOutcome::PartiallySucceeded->value,
'counts' => $counts,
];
}
private function runOutcome(
int $driftFindingsCount,
int $verifiedSubjectCount,
int $blockingSubjectCount,
int $failedSubjectCount,
bool $hasWarnings,
): CompareRunOutcome {
if ($failedSubjectCount > 0 && $verifiedSubjectCount === 0 && $driftFindingsCount === 0) {
return CompareRunOutcome::Failed;
}
if ($blockingSubjectCount > 0 && $verifiedSubjectCount === 0 && $driftFindingsCount === 0) {
return CompareRunOutcome::Blocked;
}
if ($hasWarnings || $blockingSubjectCount > 0 || $failedSubjectCount > 0) {
return CompareRunOutcome::Partial;
}
return $driftFindingsCount > 0
? CompareRunOutcome::CompletedWithDrift
: CompareRunOutcome::Completed;
}
/**
* @param array<string, int> $counts
*/
private function increment(array &$counts, ?string $key): void
{
if ($key === null) {
return;
}
$counts[$key] = ($counts[$key] ?? 0) + 1;
}
private function nonEmptyString(mixed $value): ?string
{
if (! is_string($value) || trim($value) === '') {
return null;
}
return trim($value);
}
}