TenantAtlas/apps/platform/app/Support/Baselines/CompareSemantics/BaselineCompareOutcomeClassifier.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

267 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Baselines\CompareSemantics;
use App\Support\Baselines\Compare\CompareState;
use App\Support\Baselines\Compare\CompareSubjectResult;
use App\Support\Baselines\Matching\MatchingOutcome;
final class BaselineCompareOutcomeClassifier
{
public function fromMatchingOutcome(MatchingOutcome $outcome): CompareSubjectOutcome
{
$explicitTrustLevel = $this->explicitTrustLevel($outcome->trust);
$reason = CompareResultReason::tryFrom($outcome->reasonCode) ?? $this->defaultMatchingReason($outcome);
$reason = $this->reasonAllowedByTrust($reason, $explicitTrustLevel);
return new CompareSubjectOutcome(
reason: $reason,
category: $reason->category(),
actionability: $reason->actionability(),
readinessImpact: $reason->readinessImpact(),
identityStatus: $this->identityStatusForMatching($outcome, $reason),
comparisonStatus: CompareResultComparisonStatus::NotCompared,
coverageStatus: $reason->coverageStatus(),
trustLevel: $this->trustLevel($outcome->trust, $reason),
subject: $outcome->subject->toArray(),
proof: $outcome->toArray()['proof'] ?? [],
);
}
/**
* @param array<string, mixed> $subject
* @param array<string, mixed> $proof
*/
public function fromReason(
string $reasonCode,
array $subject = [],
array $proof = [],
?CompareState $state = null,
?string $trustLevel = null,
): CompareSubjectOutcome {
$explicitTrustLevel = $this->explicitTrustLevel($trustLevel);
$reason = CompareResultReason::tryFrom($reasonCode)
?? $this->defaultReasonForState($state, $explicitTrustLevel);
$reason = $this->reasonAllowedByTrust($reason, $explicitTrustLevel);
return new CompareSubjectOutcome(
reason: $reason,
category: $reason->category(),
actionability: $reason->actionability(),
readinessImpact: $reason->readinessImpact(),
identityStatus: $this->identityStatusForReason($reason),
comparisonStatus: $this->comparisonStatusForReason($reason, $state),
coverageStatus: $reason->coverageStatus(),
trustLevel: $this->trustLevel($trustLevel, $reason),
subject: $subject,
proof: $proof,
);
}
public function fromStrategyResult(CompareSubjectResult $result): CompareSubjectOutcome
{
$explicitTrustLevel = $this->explicitTrustLevel($result->trustLevel);
$reason = match ($result->compareState) {
CompareState::NoDrift => $this->trustedVerifiedReason(
$explicitTrustLevel,
CompareResultReason::VerifiedNoDrift,
),
CompareState::Drift => $this->trustedVerifiedReason(
$explicitTrustLevel,
CompareResultReason::VerifiedDriftDetected,
),
default => CompareResultReason::tryFrom((string) ($result->gapReasonCode() ?? ''))
?? $this->defaultReasonForState($result->compareState, $explicitTrustLevel),
};
$reason = $this->reasonAllowedByTrust($reason, $explicitTrustLevel);
return new CompareSubjectOutcome(
reason: $reason,
category: $reason->category(),
actionability: $reason->actionability(),
readinessImpact: $reason->readinessImpact(),
identityStatus: $this->identityStatusForReason($reason),
comparisonStatus: $this->comparisonStatusForReason($reason, $result->compareState),
coverageStatus: $reason->coverageStatus(),
trustLevel: $this->trustLevel($result->trustLevel, $reason),
subject: [
'domain_key' => $result->subjectIdentity->domainKey,
'subject_class' => $result->subjectIdentity->subjectClass,
'subject_type_key' => $result->subjectIdentity->subjectTypeKey,
'external_subject_id' => $result->subjectIdentity->externalSubjectId,
'subject_key' => $result->subjectIdentity->subjectKey,
'operator_label' => $result->projection->operatorLabel,
],
proof: [
'compare_state' => $result->compareState->value,
'baseline_availability' => $result->baselineAvailability,
'current_state_availability' => $result->currentStateAvailability,
'evidence_quality' => $result->evidenceQuality,
'strategy_key' => is_string($result->diagnostics['strategy_key'] ?? null)
? $result->diagnostics['strategy_key']
: null,
],
);
}
private function defaultMatchingReason(MatchingOutcome $outcome): CompareResultReason
{
return match ($outcome->status) {
MatchingOutcome::Resolved => CompareResultReason::ResolvedProviderIdentity,
MatchingOutcome::Ambiguous => CompareResultReason::UnresolvedDuplicateCandidates,
MatchingOutcome::MissingProviderResource => CompareResultReason::MissingProviderResource,
MatchingOutcome::MissingLocalEvidence => CompareResultReason::MissingLocalEvidence,
MatchingOutcome::UnresolvedIdentity => CompareResultReason::IdentityRequired,
MatchingOutcome::Unsupported => CompareResultReason::UnsupportedResourceClass,
MatchingOutcome::Limited => CompareResultReason::AcceptedLimitation,
MatchingOutcome::Excluded => CompareResultReason::ExcludedNonGoverned,
default => CompareResultReason::CompareFailed,
};
}
private function defaultReasonForState(
?CompareState $state,
?CompareResultTrustLevel $explicitTrustLevel = null,
): CompareResultReason {
if ($state === CompareState::NoDrift) {
return $this->trustedVerifiedReason($explicitTrustLevel, CompareResultReason::VerifiedNoDrift);
}
if ($state === CompareState::Drift) {
return $this->trustedVerifiedReason($explicitTrustLevel, CompareResultReason::VerifiedDriftDetected);
}
return match ($state) {
CompareState::Unsupported => CompareResultReason::CompareNotSupported,
CompareState::Ambiguous => CompareResultReason::UnresolvedAmbiguousIdentity,
CompareState::Failed => CompareResultReason::CompareFailed,
default => CompareResultReason::MissingLocalEvidence,
};
}
private function trustedVerifiedReason(
?CompareResultTrustLevel $explicitTrustLevel,
CompareResultReason $verifiedReason,
): CompareResultReason {
return $this->reasonAllowedByTrust($verifiedReason, $explicitTrustLevel);
}
private function reasonAllowedByTrust(
CompareResultReason $reason,
?CompareResultTrustLevel $explicitTrustLevel,
): CompareResultReason {
if (! $this->requiresTrustedComparison($reason)) {
return $reason;
}
return $this->allowsVerifiedComparison($explicitTrustLevel)
? $reason
: CompareResultReason::UnresolvedLowTrustMatch;
}
private function allowsVerifiedComparison(?CompareResultTrustLevel $trustLevel): bool
{
return in_array($trustLevel, [
CompareResultTrustLevel::High,
CompareResultTrustLevel::Medium,
], true);
}
private function requiresTrustedComparison(CompareResultReason $reason): bool
{
return in_array($reason, [
CompareResultReason::VerifiedNoDrift,
CompareResultReason::VerifiedDriftDetected,
CompareResultReason::ResolvedActiveBinding,
CompareResultReason::ResolvedCanonicalIdentity,
CompareResultReason::ResolvedProviderIdentity,
], true);
}
private function identityStatusForMatching(MatchingOutcome $outcome, CompareResultReason $reason): CompareResultIdentityStatus
{
return match ($reason) {
CompareResultReason::ResolvedActiveBinding => CompareResultIdentityStatus::BindingResolved,
CompareResultReason::ResolvedCanonicalIdentity => CompareResultIdentityStatus::CanonicalizationResolved,
CompareResultReason::ResolvedProviderIdentity,
CompareResultReason::VerifiedNoDrift,
CompareResultReason::VerifiedDriftDetected => CompareResultIdentityStatus::Resolved,
CompareResultReason::MissingLocalEvidence,
CompareResultReason::MissingProviderResource => CompareResultIdentityStatus::Missing,
CompareResultReason::UnsupportedResourceClass,
CompareResultReason::CompareNotSupported => CompareResultIdentityStatus::Unsupported,
default => $outcome->isComparable()
? CompareResultIdentityStatus::Resolved
: CompareResultIdentityStatus::Unresolved,
};
}
private function identityStatusForReason(CompareResultReason $reason): CompareResultIdentityStatus
{
return match ($reason) {
CompareResultReason::ResolvedActiveBinding => CompareResultIdentityStatus::BindingResolved,
CompareResultReason::ResolvedCanonicalIdentity => CompareResultIdentityStatus::CanonicalizationResolved,
CompareResultReason::ResolvedProviderIdentity,
CompareResultReason::VerifiedNoDrift,
CompareResultReason::VerifiedDriftDetected => CompareResultIdentityStatus::Resolved,
CompareResultReason::MissingLocalEvidence,
CompareResultReason::MissingProviderResource => CompareResultIdentityStatus::Missing,
CompareResultReason::UnsupportedResourceClass,
CompareResultReason::CompareNotSupported => CompareResultIdentityStatus::Unsupported,
default => CompareResultIdentityStatus::Unresolved,
};
}
private function comparisonStatusForReason(CompareResultReason $reason, ?CompareState $state): CompareResultComparisonStatus
{
return match ($reason) {
CompareResultReason::VerifiedNoDrift => CompareResultComparisonStatus::NoDrift,
CompareResultReason::VerifiedDriftDetected => CompareResultComparisonStatus::DriftDetected,
CompareResultReason::CompareFailed => CompareResultComparisonStatus::CompareFailed,
CompareResultReason::CompareNotSupported,
CompareResultReason::UnsupportedResourceClass => CompareResultComparisonStatus::CompareNotSupported,
default => CompareResultComparisonStatus::NotCompared,
};
}
private function trustLevel(?string $trustLevel, CompareResultReason $reason): CompareResultTrustLevel
{
return match (strtolower((string) $trustLevel)) {
'authoritative',
'trustworthy',
'high' => CompareResultTrustLevel::High,
'limited_confidence',
'limited',
'medium' => CompareResultTrustLevel::Medium,
'low' => CompareResultTrustLevel::Low,
'unusable',
'none',
'untrusted' => CompareResultTrustLevel::Untrusted,
'failed' => CompareResultTrustLevel::Failed,
'not_applicable' => CompareResultTrustLevel::NotApplicable,
default => $reason->defaultTrustLevel(),
};
}
private function explicitTrustLevel(?string $trustLevel): ?CompareResultTrustLevel
{
return match (strtolower((string) $trustLevel)) {
'authoritative',
'trustworthy',
'high' => CompareResultTrustLevel::High,
'limited_confidence',
'limited',
'medium' => CompareResultTrustLevel::Medium,
'low' => CompareResultTrustLevel::Low,
'unusable',
'none',
'untrusted' => CompareResultTrustLevel::Untrusted,
'failed' => CompareResultTrustLevel::Failed,
'not_applicable' => CompareResultTrustLevel::NotApplicable,
default => null,
};
}
}