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
771 lines
30 KiB
PHP
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;
|
|
}
|
|
}
|