TenantAtlas/apps/platform/app/Services/Baselines/BaselineSubjectResolutionQuery.php
ahmido 39298f27f2 feat(ui): implement baseline subject resolution ui (#455)
Added `BaselineSubjectResolution` page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages as defined in Spec 384. Replaces legacy compare warnings with an actionable, deterministic UI surface.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #455
2026-06-16 23:36:38 +00:00

771 lines
30 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Baselines;
use App\Models\InventoryItem;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderResourceBinding;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\Baselines\CompareSemantics\CompareResultActionability;
use App\Support\Baselines\SubjectClass;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\OperationCatalog;
use App\Support\OperationRunType;
use App\Support\Resources\ProviderResourceBindingStatus;
use App\Support\Resources\ProviderResourceDescriptor;
use App\Support\Resources\ResourceIdentity;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
final class BaselineSubjectResolutionQuery
{
/**
* @param array<string, mixed> $filters
* @return list<array<string, mixed>>
*/
public function rows(ManagedEnvironment $environment, array $filters = []): array
{
$run = $this->resolveRun($environment, $this->intFilter($filters, 'operation_run_id'));
if (! $run instanceof OperationRun) {
return [];
}
$outcomes = $this->subjectOutcomes($run);
if ($outcomes === []) {
return [];
}
$activeBindings = $this->activeBindings($environment);
$inventoryDescriptors = $this->inventoryDescriptors($environment);
$rows = collect($outcomes)
->filter(fn (array $outcome): bool => $this->includeOutcome($outcome, $filters))
->values()
->map(fn (array $outcome, int $index): array => $this->rowFromOutcome(
outcome: $outcome,
index: $index,
run: $run,
environment: $environment,
activeBindings: $activeBindings,
inventoryDescriptors: $inventoryDescriptors,
))
->filter(fn (array $row): bool => $this->matchesFilters($row, $filters))
->sortBy([
fn (array $row): int => $this->readinessSortWeight((string) ($row['readiness_impact'] ?? '')),
fn (array $row): int => $row['active_binding_id'] !== null ? 1 : 0,
fn (array $row): string => (string) ($row['subject_label'] ?? ''),
])
->values();
return $rows->all();
}
/**
* @param array<string, mixed> $filters
*/
public function row(ManagedEnvironment $environment, string $rowId, array $filters = []): ?array
{
return collect($this->rows($environment, $filters))
->first(fn (array $row): bool => (string) ($row['id'] ?? '') === $rowId);
}
/**
* @return array{
* has_run: bool,
* source_operation_run_id: int|null,
* actionable_count: int,
* visible_count: int,
* by_actionability: array<string, int>,
* by_readiness_impact: array<string, int>,
* by_reason: array<string, int>,
* legacy_payload_only: bool
* }
*/
public function summary(ManagedEnvironment $environment, ?int $operationRunId = null): array
{
$run = $this->resolveRun($environment, $operationRunId);
$rows = $run instanceof OperationRun
? $this->rows($environment, ['operation_run_id' => (int) $run->getKey()])
: [];
$outcomes = $run instanceof OperationRun ? $this->subjectOutcomes($run) : [];
return [
'has_run' => $run instanceof OperationRun,
'source_operation_run_id' => $run instanceof OperationRun ? (int) $run->getKey() : null,
'actionable_count' => count($rows),
'visible_count' => count($rows),
'by_actionability' => $this->countBy($rows, 'actionability'),
'by_readiness_impact' => $this->countBy($rows, 'readiness_impact'),
'by_reason' => $this->countBy($rows, 'reason'),
'legacy_payload_only' => $run instanceof OperationRun
&& $outcomes === []
&& is_array(data_get($run->context, 'baseline_compare.evidence_gaps')),
];
}
public function resolveRun(ManagedEnvironment $environment, ?int $operationRunId = null): ?OperationRun
{
$query = OperationRun::query()
->where('workspace_id', (int) $environment->workspace_id)
->where('managed_environment_id', (int) $environment->getKey())
->whereIn('type', OperationCatalog::rawValuesForCanonical(OperationRunType::BaselineCompare->value));
if ($operationRunId !== null && $operationRunId > 0) {
return $query->whereKey($operationRunId)->first();
}
return $query
->latest('completed_at')
->latest('id')
->first();
}
/**
* @return array<string, string>
*/
public function filterOptions(ManagedEnvironment $environment, ?int $operationRunId = null, string $key = 'all'): array
{
$rows = collect($this->rows($environment, array_filter([
'operation_run_id' => $operationRunId,
'include_resolved' => true,
], static fn (mixed $value): bool => $value !== null)));
$map = match ($key) {
'provider' => $rows->pluck('provider_label', 'provider_key'),
'subject_class' => $rows->pluck('subject_class_label', 'subject_class'),
'resource_type' => $rows->pluck('resource_type_label', 'subject_type_key'),
'actionability' => $rows->pluck('actionability_label', 'actionability'),
'readiness_impact' => $rows->pluck('readiness_label', 'readiness_impact'),
'reason' => $rows->pluck('reason_label', 'reason'),
default => collect(),
};
return $map
->filter(fn (mixed $label, mixed $value): bool => is_string($value) && $value !== '' && is_string($label) && $label !== '')
->unique()
->sort()
->all();
}
/**
* @return list<array<string, mixed>>
*/
private function subjectOutcomes(OperationRun $run): array
{
$outcomes = data_get($run->context, 'baseline_compare.result_semantics.subject_outcomes');
if (! is_array($outcomes)) {
return [];
}
return collect($outcomes)
->filter(fn (mixed $outcome): bool => is_array($outcome))
->values()
->all();
}
/**
* @return EloquentCollection<int, ProviderResourceBinding>
*/
private function activeBindings(ManagedEnvironment $environment): EloquentCollection
{
return ProviderResourceBinding::query()
->where('workspace_id', (int) $environment->workspace_id)
->where('managed_environment_id', (int) $environment->getKey())
->where('binding_status', ProviderResourceBindingStatus::Active->value)
->latest('decided_at')
->get();
}
/**
* @return Collection<int, ProviderResourceDescriptor>
*/
private function inventoryDescriptors(ManagedEnvironment $environment): Collection
{
return InventoryItem::query()
->where('workspace_id', (int) $environment->workspace_id)
->where('managed_environment_id', (int) $environment->getKey())
->latest('last_seen_at')
->get()
->map(fn (InventoryItem $item): ?ProviderResourceDescriptor => $this->descriptorFromInventoryItem($item))
->filter()
->values();
}
/**
* @param EloquentCollection<int, ProviderResourceBinding> $activeBindings
* @param Collection<int, ProviderResourceDescriptor> $inventoryDescriptors
* @return array<string, mixed>
*/
private function rowFromOutcome(
array $outcome,
int $index,
OperationRun $run,
ManagedEnvironment $environment,
EloquentCollection $activeBindings,
Collection $inventoryDescriptors,
): array {
$subject = is_array($outcome['subject'] ?? null) ? $outcome['subject'] : [];
$proof = is_array($outcome['proof'] ?? null) ? $outcome['proof'] : [];
$subjectTypeKey = $this->stringValue(
$subject['subject_type_key'] ?? $subject['policy_type'] ?? $proof['policy_type'] ?? null,
) ?? 'unknown';
$subjectClass = $this->stringValue($subject['subject_class'] ?? null) ?? SubjectClass::PolicyBacked->value;
$subjectDomain = $this->stringValue($subject['subject_domain'] ?? $subject['domain_key'] ?? null) ?? 'baseline';
$subjectKey = $this->stringValue($subject['canonical_subject_key'] ?? $subject['subject_key'] ?? null);
$canonicalSubjectKey = $this->canonicalSubjectKey($subject);
$displayLabel = $this->stringValue(
$subject['display_label']
?? $subject['operator_label']
?? $subject['subject_key']
?? $subject['external_subject_id']
?? null,
);
$providerDescriptor = $this->descriptorFromSubject($subject);
$decisionIdentity = $providerDescriptor?->identity;
$candidates = $this->candidateRows(
subject: $subject,
subjectTypeKey: $subjectTypeKey,
subjectClass: $subjectClass,
canonicalSubjectKey: $canonicalSubjectKey,
outcomeDescriptors: $this->candidateDescriptorsFromOutcome($outcome),
inventoryDescriptors: $inventoryDescriptors,
);
$activeBinding = $this->activeBindingFor(
activeBindings: $activeBindings,
canonicalSubjectKey: $canonicalSubjectKey,
decisionIdentity: $decisionIdentity,
);
$providerKey = $decisionIdentity?->providerKey
?? ($candidates[0]['provider_key'] ?? null)
?? $this->stringValue($subject['provider_key'] ?? $proof['provider_key'] ?? null)
?? 'unknown';
$reason = $this->stringValue($outcome['reason'] ?? null) ?? 'unknown';
$actionability = $this->stringValue($outcome['actionability'] ?? null) ?? 'unknown';
$readinessImpact = $this->stringValue($outcome['readiness_impact'] ?? null) ?? 'unknown';
$rowId = $this->rowId($run, $index, $reason, $subjectTypeKey, $subjectKey, $canonicalSubjectKey);
$sourceReferences = is_array($subject['source_references'] ?? null) ? $subject['source_references'] : [];
return [
'id' => $rowId,
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'source_operation_run_id' => (int) $run->getKey(),
'source_baseline_snapshot_id' => is_numeric(data_get($run->context, 'baseline_snapshot_id'))
? (int) data_get($run->context, 'baseline_snapshot_id')
: null,
'subject_domain' => $subjectDomain,
'subject_class' => $subjectClass,
'subject_class_label' => $this->label($subjectClass),
'subject_type_key' => $subjectTypeKey,
'resource_type_label' => InventoryPolicyTypeMeta::baselineCompareLabel($subjectTypeKey)
?? InventoryPolicyTypeMeta::label($subjectTypeKey)
?? $this->label($subjectTypeKey),
'subject_key' => $subjectKey,
'canonical_subject_key' => $canonicalSubjectKey,
'subject_label' => $displayLabel ?: ($subjectKey ?: $this->label($subjectTypeKey)),
'provider_key' => (string) $providerKey,
'provider_label' => $this->label((string) $providerKey),
'reason' => $reason,
'reason_label' => $this->label($reason),
'actionability' => $actionability,
'actionability_label' => $this->actionabilityLabel($actionability),
'readiness_impact' => $readinessImpact,
'readiness_label' => $this->readinessLabel($readinessImpact),
'identity_status' => $this->stringValue($outcome['identity_status'] ?? null),
'comparison_status' => $this->stringValue($outcome['comparison_status'] ?? null),
'coverage_status' => $this->stringValue($outcome['coverage_status'] ?? null),
'trust_level' => $this->stringValue($outcome['trust_level'] ?? null),
'candidate_count' => count($candidates),
'candidates' => $candidates,
'has_candidates' => $candidates !== [],
'decision_identity' => $decisionIdentity?->toArray(),
'active_binding_id' => $activeBinding instanceof ProviderResourceBinding ? (int) $activeBinding->getKey() : null,
'active_binding_mode' => $activeBinding instanceof ProviderResourceBinding
? $this->enumValue($activeBinding->resolution_mode)
: null,
'current_decision_label' => $activeBinding instanceof ProviderResourceBinding
? $this->label($this->enumValue($activeBinding->resolution_mode))
: 'None recorded',
'source_inventory_item_id' => is_numeric($sourceReferences['inventory_item_id'] ?? null)
? (int) $sourceReferences['inventory_item_id']
: null,
'source_policy_version_id' => is_numeric($sourceReferences['policy_version_id'] ?? null)
? (int) $sourceReferences['policy_version_id']
: null,
'last_seen' => $candidates[0]['last_seen_at'] ?? null,
'search_text' => Str::lower(implode(' ', array_filter([
$displayLabel,
$subjectKey,
$canonicalSubjectKey,
$subjectTypeKey,
$subjectClass,
$providerKey,
$reason,
$actionability,
$readinessImpact,
$activeBinding?->display_label,
]))),
];
}
/**
* @return Collection<int, ProviderResourceDescriptor>
*/
private function candidateDescriptorsFromOutcome(array $outcome): Collection
{
return collect([
data_get($outcome, 'candidate_descriptors'),
data_get($outcome, 'subject.candidate_descriptors'),
data_get($outcome, 'proof.candidate_descriptors'),
data_get($outcome, 'candidates'),
data_get($outcome, 'subject.candidates'),
data_get($outcome, 'proof.candidates'),
])
->flatMap(function (mixed $payload): array {
if (! is_array($payload)) {
return [];
}
return array_is_list($payload) ? $payload : [$payload];
})
->map(fn (mixed $payload): ?ProviderResourceDescriptor => is_array($payload)
? $this->descriptorFromPayload($payload)
: null)
->filter()
->values();
}
/**
* @param Collection<int, ProviderResourceDescriptor> $outcomeDescriptors
* @param Collection<int, ProviderResourceDescriptor> $inventoryDescriptors
* @return list<array<string, mixed>>
*/
private function candidateRows(
array $subject,
string $subjectTypeKey,
string $subjectClass,
?string $canonicalSubjectKey,
Collection $outcomeDescriptors,
Collection $inventoryDescriptors,
): array {
$subjectProviderKey = $this->stringValue(data_get($subject, 'provider_resource_descriptor.identity.provider_key'));
$outcomeRows = $outcomeDescriptors
->filter(fn (ProviderResourceDescriptor $descriptor): bool => $this->descriptorMatchesCandidateScope(
descriptor: $descriptor,
subjectTypeKey: $subjectTypeKey,
subjectClass: $subjectClass,
canonicalSubjectKey: $canonicalSubjectKey,
subjectProviderKey: $subjectProviderKey,
requireCanonicalMatch: false,
))
->map(fn (ProviderResourceDescriptor $descriptor): array => $this->candidateRow($descriptor))
->values();
$inventoryRows = $inventoryDescriptors
->filter(fn (ProviderResourceDescriptor $descriptor): bool => $this->descriptorMatchesCandidateScope(
descriptor: $descriptor,
subjectTypeKey: $subjectTypeKey,
subjectClass: $subjectClass,
canonicalSubjectKey: $canonicalSubjectKey,
subjectProviderKey: $subjectProviderKey,
requireCanonicalMatch: true,
))
->map(fn (ProviderResourceDescriptor $descriptor): array => $this->candidateRow($descriptor))
->values();
return $outcomeRows
->merge($inventoryRows)
->unique('candidate_key')
->values()
->all();
}
private function descriptorMatchesCandidateScope(
ProviderResourceDescriptor $descriptor,
string $subjectTypeKey,
string $subjectClass,
?string $canonicalSubjectKey,
?string $subjectProviderKey,
bool $requireCanonicalMatch,
): bool {
if ($descriptor->subjectTypeKey !== $subjectTypeKey) {
return false;
}
$descriptorClass = $descriptor->subjectClass instanceof SubjectClass
? $descriptor->subjectClass->value
: (string) $descriptor->subjectClass;
if ($subjectClass !== '' && $descriptorClass !== $subjectClass) {
return false;
}
if ($subjectProviderKey !== null && $descriptor->identity->providerKey !== $subjectProviderKey) {
return false;
}
$descriptorCanonicalKey = BaselineSubjectKey::forProviderResourceIdentity(
$descriptor->subjectDomain,
$descriptor->subjectClass,
$descriptor->subjectTypeKey,
$descriptor->identity,
);
if ($canonicalSubjectKey !== null && $descriptorCanonicalKey === $canonicalSubjectKey) {
return true;
}
return ! $requireCanonicalMatch;
}
/**
* @return array<string, mixed>
*/
private function candidateRow(ProviderResourceDescriptor $descriptor): array
{
$identity = $descriptor->identity;
$sourceReferences = $descriptor->sourceReferences;
return [
'candidate_key' => $identity->fingerprint(),
'identity' => $identity->toArray(),
'display_label' => $descriptor->displayLabel ?: $this->label((string) ($identity->providerResourceType ?? 'resource')),
'provider_key' => $identity->providerKey,
'provider_label' => $this->label($identity->providerKey),
'provider_resource_type' => $identity->providerResourceType,
'identity_kind' => $identity->identityKind,
'stable_identity_preview' => $this->preview($identity->stableIdentityValue()),
'source_inventory_item_id' => is_numeric($sourceReferences['inventory_item_id'] ?? null)
? (int) $sourceReferences['inventory_item_id']
: null,
'source_policy_version_id' => is_numeric($sourceReferences['policy_version_id'] ?? null)
? (int) $sourceReferences['policy_version_id']
: null,
'last_seen_at' => $descriptor->lastSeenAt,
];
}
private function descriptorFromSubject(array $subject): ?ProviderResourceDescriptor
{
$descriptorPayload = $subject['provider_resource_descriptor'] ?? null;
return is_array($descriptorPayload) ? $this->descriptorFromPayload($descriptorPayload) : null;
}
/**
* @param array<string, mixed> $payload
*/
private function descriptorFromPayload(array $payload): ?ProviderResourceDescriptor
{
$descriptorPayload = $payload['provider_resource_descriptor'] ?? $payload['descriptor'] ?? $payload;
if (! is_array($descriptorPayload)) {
return null;
}
try {
return ProviderResourceDescriptor::fromArray($descriptorPayload);
} catch (InvalidArgumentException) {
return null;
}
}
private function descriptorFromInventoryItem(InventoryItem $inventoryItem): ?ProviderResourceDescriptor
{
$metaJsonb = is_array($inventoryItem->meta_jsonb) ? $inventoryItem->meta_jsonb : [];
$descriptorPayload = $metaJsonb['provider_resource_descriptor'] ?? null;
if (is_array($descriptorPayload)) {
try {
return ProviderResourceDescriptor::fromArray($descriptorPayload);
} catch (InvalidArgumentException) {
return null;
}
}
$identity = $this->resourceIdentityFromMeta(
metaJsonb: $metaJsonb,
fallbackProviderKey: $this->stringValue($metaJsonb['provider_key'] ?? $metaJsonb['provider'] ?? null) ?? 'inventory',
fallbackResourceType: $this->stringValue($metaJsonb['provider_resource_type'] ?? $metaJsonb['resource_type'] ?? null)
?? (string) $inventoryItem->policy_type,
fallbackResourceId: $this->stringValue($inventoryItem->external_id),
);
if (! $identity instanceof ResourceIdentity) {
return null;
}
return ProviderResourceDescriptor::fromIdentity(
identity: $identity,
subjectDomain: $this->stringValue($metaJsonb['subject_domain'] ?? null) ?? 'baseline',
subjectClass: $this->stringValue($metaJsonb['subject_class'] ?? null) ?? SubjectClass::PolicyBacked->value,
subjectTypeKey: (string) $inventoryItem->policy_type,
displayLabel: $this->stringValue($inventoryItem->display_name)
?? $this->stringValue($metaJsonb['display_name'] ?? null),
sourceReferences: [
'inventory_item_id' => (int) $inventoryItem->getKey(),
'external_id' => (string) $inventoryItem->external_id,
],
fingerprint: $this->stringValue($metaJsonb['provider_resource_fingerprint'] ?? null) ?? $identity->fingerprint(),
lastSeenAt: $inventoryItem->last_seen_at?->toIso8601String(),
);
}
/**
* @param array<string, mixed> $metaJsonb
*/
private function resourceIdentityFromMeta(
array $metaJsonb,
?string $fallbackProviderKey = null,
?string $fallbackResourceType = null,
?string $fallbackResourceId = null,
): ?ResourceIdentity {
$descriptorPayload = $metaJsonb['provider_resource_descriptor'] ?? null;
$identityPayload = is_array($descriptorPayload) ? ($descriptorPayload['identity'] ?? null) : null;
if (! is_array($identityPayload)) {
$identityPayload = $metaJsonb['provider_resource_identity'] ?? null;
}
if (is_array($identityPayload)) {
try {
return ResourceIdentity::fromArray($identityPayload);
} catch (InvalidArgumentException) {
return null;
}
}
$providerKey = $this->stringValue($metaJsonb['provider_key'] ?? $metaJsonb['provider'] ?? null) ?? $fallbackProviderKey;
$resourceType = $this->stringValue($metaJsonb['provider_resource_type'] ?? $metaJsonb['resource_type'] ?? $metaJsonb['provider_object_type'] ?? null)
?? $fallbackResourceType;
$resourceId = $this->stringValue($metaJsonb['provider_resource_id'] ?? $metaJsonb['resource_id'] ?? null) ?? $fallbackResourceId;
$discriminator = $this->stringValue($metaJsonb['provider_resource_discriminator'] ?? $metaJsonb['canonical_discriminator'] ?? null);
$identityKind = $this->stringValue($metaJsonb['provider_resource_identity_kind'] ?? $metaJsonb['identity_kind'] ?? null)
?? ResourceIdentity::ProviderResource;
if ($providerKey === null || $resourceType === null) {
return null;
}
try {
return new ResourceIdentity(
providerKey: $providerKey,
identityKind: $identityKind,
providerResourceType: $resourceType,
providerResourceId: $identityKind === ResourceIdentity::ProviderResource ? $resourceId : null,
canonicalDiscriminator: $identityKind === ResourceIdentity::ProviderResource ? null : $discriminator,
);
} catch (InvalidArgumentException) {
return null;
}
}
/**
* @param EloquentCollection<int, ProviderResourceBinding> $activeBindings
*/
private function activeBindingFor(
EloquentCollection $activeBindings,
?string $canonicalSubjectKey,
?ResourceIdentity $decisionIdentity,
): ?ProviderResourceBinding {
return $activeBindings->first(function (ProviderResourceBinding $binding) use ($canonicalSubjectKey, $decisionIdentity): bool {
if ($canonicalSubjectKey !== null && (string) $binding->canonical_subject_key === $canonicalSubjectKey) {
return true;
}
return $decisionIdentity instanceof ResourceIdentity
&& (string) $binding->provider_key === $decisionIdentity->providerKey
&& (string) $binding->provider_resource_fingerprint === $decisionIdentity->fingerprint();
});
}
private function canonicalSubjectKey(array $subject): ?string
{
$candidate = $this->stringValue($subject['canonical_subject_key'] ?? $subject['subject_key'] ?? null);
return BaselineSubjectKey::isProviderResourceCanonicalKey($candidate) ? $candidate : null;
}
/**
* @param array<string, mixed> $filters
*/
private function includeOutcome(array $outcome, array $filters): bool
{
if ((bool) ($filters['include_resolved'] ?? false)) {
return true;
}
$actionability = $this->stringValue($outcome['actionability'] ?? null);
return ! in_array($actionability, [
CompareResultActionability::None->value,
CompareResultActionability::Accepted->value,
CompareResultActionability::Excluded->value,
], true);
}
/**
* @param array<string, mixed> $filters
*/
private function matchesFilters(array $row, array $filters): bool
{
$stringFilters = [
'provider' => 'provider_key',
'subject_class' => 'subject_class',
'resource_type' => 'subject_type_key',
'actionability' => 'actionability',
'readiness_impact' => 'readiness_impact',
'reason' => 'reason',
];
foreach ($stringFilters as $filterKey => $rowKey) {
$filterValue = $this->stringFilter($filters, $filterKey);
if ($filterValue !== null && (string) ($row[$rowKey] ?? '') !== $filterValue) {
return false;
}
}
$activeBinding = $this->stringFilter($filters, 'active_binding');
if ($activeBinding === 'yes' && $row['active_binding_id'] === null) {
return false;
}
if ($activeBinding === 'no' && $row['active_binding_id'] !== null) {
return false;
}
$candidates = $this->stringFilter($filters, 'candidates');
if ($candidates === 'yes' && ! (bool) ($row['has_candidates'] ?? false)) {
return false;
}
if ($candidates === 'no' && (bool) ($row['has_candidates'] ?? false)) {
return false;
}
return true;
}
/**
* @param list<array<string, mixed>> $rows
* @return array<string, int>
*/
private function countBy(array $rows, string $key): array
{
return collect($rows)
->countBy(fn (array $row): string => (string) ($row[$key] ?? 'unknown'))
->sortKeys()
->all();
}
private function rowId(OperationRun $run, int $index, string $reason, string $subjectTypeKey, ?string $subjectKey, ?string $canonicalSubjectKey): string
{
return hash('sha256', implode('|', [
(string) $run->getKey(),
(string) $index,
$reason,
$subjectTypeKey,
(string) ($canonicalSubjectKey ?? $subjectKey ?? 'subject'),
]));
}
private function intFilter(array $filters, string $key): ?int
{
$value = $filters[$key] ?? null;
return is_numeric($value) ? (int) $value : null;
}
private function stringFilter(array $filters, string $key): ?string
{
$value = $filters[$key] ?? null;
$value = is_array($value) ? ($value['value'] ?? null) : $value;
return $this->stringValue($value);
}
private function stringValue(mixed $value): ?string
{
if (! is_string($value) && ! is_numeric($value)) {
return null;
}
$value = trim((string) $value);
return $value !== '' ? $value : null;
}
private function enumValue(mixed $value): string
{
if ($value instanceof \BackedEnum) {
return (string) $value->value;
}
return (string) $value;
}
private function label(string $value): string
{
$value = trim($value);
return $value === '' ? 'Unknown' : Str::of($value)->replace(['_', '-'], ' ')->headline()->toString();
}
private function actionabilityLabel(string $actionability): string
{
return match ($actionability) {
CompareResultActionability::BindingRequired->value => 'Binding required',
CompareResultActionability::OperatorActionRequired->value => 'Operator decision required',
CompareResultActionability::ProviderDataRefreshRequired->value => 'Refresh provider data',
CompareResultActionability::ImplementationGap->value => 'Implementation gap',
CompareResultActionability::ScopeDecisionRequired->value => 'Scope decision required',
default => $this->label($actionability),
};
}
private function readinessLabel(string $readinessImpact): string
{
return match ($readinessImpact) {
'customer_blocker' => 'Customer blocker',
'internal_blocker' => 'Internal blocker',
'customer_limitation' => 'Customer limitation',
'internal_limitation' => 'Internal limitation',
'no_impact' => 'No impact',
default => $this->label($readinessImpact),
};
}
private function readinessSortWeight(string $readinessImpact): int
{
return match ($readinessImpact) {
'customer_blocker' => 0,
'internal_blocker' => 1,
'customer_limitation' => 2,
'internal_limitation' => 3,
default => 4,
};
}
private function preview(?string $value): ?string
{
if ($value === null || trim($value) === '') {
return null;
}
$value = trim($value);
return Str::length($value) > 16 ? Str::substr($value, 0, 10).'...'.Str::substr($value, -4) : $value;
}
}