Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m12s
Added BaselineSubjectResolution page and supporting logic to visualize missing identities, ambiguous matches, and skipped coverages per Spec 384.
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;
|
|
}
|
|
}
|