TenantAtlas/apps/platform/app/Support/Baselines/Compare/CompareSubjectResult.php
ahmido d644265d30 Spec 203: extract baseline compare strategy (#233)
## Summary
- extract baseline compare orchestration behind an explicit strategy contract and registry
- preserve the current Intune compare path through a dedicated `IntuneCompareStrategy`
- harden compare launch and review surfaces for mixed, unsupported, incomplete, and strategy-failure truth
- add Spec 203 artifacts, focused regression coverage, and future-domain strategy proof tests

## Testing
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Baselines/CompareStrategyRegistryTest.php tests/Unit/Baselines/CompareSubjectResultContractTest.php tests/Feature/Baselines/BaselineCompareStrategySelectionTest.php tests/Feature/Baselines/BaselineComparePreconditionsTest.php tests/Feature/Baselines/BaselineCompareExecutionGuardTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php`
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`

## Notes
- no new Filament panel/provider registration changes
- no global-search resource changes
- no new asset registration or deployment step changes

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #233
2026-04-13 21:17:04 +00:00

119 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Baselines\Compare;
use InvalidArgumentException;
final class CompareSubjectResult
{
/**
* @param array<string, mixed> $diagnostics
*/
public function __construct(
public readonly CompareSubjectIdentity $subjectIdentity,
public readonly CompareSubjectProjection $projection,
public readonly string $baselineAvailability,
public readonly string $currentStateAvailability,
public readonly CompareState $compareState,
public readonly string $trustLevel,
public readonly string $evidenceQuality,
public readonly ?string $severityRecommendation = null,
public readonly ?CompareFindingCandidate $findingCandidate = null,
public readonly array $diagnostics = [],
) {
if (trim($this->baselineAvailability) === '' || trim($this->currentStateAvailability) === '' || trim($this->trustLevel) === '' || trim($this->evidenceQuality) === '') {
throw new InvalidArgumentException('Compare subject results require non-empty availability, trust level, and evidence quality values.');
}
if ($this->compareState === CompareState::Drift && ! $this->findingCandidate instanceof CompareFindingCandidate) {
throw new InvalidArgumentException('Drift compare subject results require a finding candidate.');
}
if ($this->compareState !== CompareState::Drift && $this->findingCandidate instanceof CompareFindingCandidate) {
throw new InvalidArgumentException('Only drift compare subject results may carry a finding candidate.');
}
}
public function hasFindingCandidate(): bool
{
return $this->findingCandidate instanceof CompareFindingCandidate;
}
public function isGapState(): bool
{
return in_array($this->compareState, [
CompareState::Unsupported,
CompareState::Incomplete,
CompareState::Ambiguous,
CompareState::Failed,
], true);
}
public function gapReasonCode(): ?string
{
$reasonCode = $this->diagnostics['reason_code'] ?? null;
return is_string($reasonCode) && trim($reasonCode) !== '' ? trim($reasonCode) : null;
}
/**
* @return array<string, mixed>|null
*/
public function gapRecord(): ?array
{
$gapRecord = $this->diagnostics['gap_record'] ?? null;
return is_array($gapRecord) ? $gapRecord : null;
}
/**
* @return array{
* subject_identity: array{
* domain_key: string,
* subject_class: string,
* subject_type_key: string,
* external_subject_id: ?string,
* subject_key: string
* },
* projection: array{
* platform_subject_class: string,
* domain_key: string,
* subject_type_key: string,
* operator_label: string,
* summary_kind: ?string,
* additional_labels: array<string, string>
* },
* baseline_availability: string,
* current_state_availability: string,
* compare_state: string,
* trust_level: string,
* evidence_quality: string,
* severity_recommendation: ?string,
* finding_candidate: ?array{
* change_type: string,
* severity: string,
* fingerprint_basis: array<string, mixed>,
* evidence_payload: array<string, mixed>,
* auto_close_eligible: bool
* },
* diagnostics: array<string, mixed>
* }
*/
public function toArray(): array
{
return [
'subject_identity' => $this->subjectIdentity->toArray(),
'projection' => $this->projection->toArray(),
'baseline_availability' => $this->baselineAvailability,
'current_state_availability' => $this->currentStateAvailability,
'compare_state' => $this->compareState->value,
'trust_level' => $this->trustLevel,
'evidence_quality' => $this->evidenceQuality,
'severity_recommendation' => $this->severityRecommendation,
'finding_candidate' => $this->findingCandidate?->toArray(),
'diagnostics' => $this->diagnostics,
];
}
}