'array', 'failure_summary' => 'array', 'context' => 'array', 'started_at' => 'datetime', 'completed_at' => 'datetime', ]; protected static function booted(): void { static::creating(function (self $operationRun): void { if ($operationRun->workspace_id !== null) { return; } if ($operationRun->tenant_id === null) { return; } $tenant = Tenant::query()->whereKey((int) $operationRun->tenant_id)->first(); if (! $tenant instanceof Tenant) { return; } if ($tenant->workspace_id === null) { return; } $operationRun->workspace_id = (int) $tenant->workspace_id; }); } public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class)->withTrashed(); } public function workspace(): BelongsTo { return $this->belongsTo(Workspace::class); } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function scopeActive(Builder $query): Builder { return $query->whereIn('status', ['queued', 'running']); } public function getSelectionHashAttribute(): ?string { $context = is_array($this->context) ? $this->context : []; return isset($context['selection_hash']) && is_string($context['selection_hash']) ? $context['selection_hash'] : null; } public function setSelectionHashAttribute(?string $value): void { $context = is_array($this->context) ? $this->context : []; $context['selection_hash'] = $value; $this->context = $context; } /** * @return array */ public function getSelectionPayloadAttribute(): array { $context = is_array($this->context) ? $this->context : []; return Arr::only($context, [ 'policy_types', 'categories', 'include_foundations', 'include_dependencies', ]); } /** * @param array|null $value */ public function setSelectionPayloadAttribute(?array $value): void { $context = is_array($this->context) ? $this->context : []; if (is_array($value)) { $context = array_merge($context, Arr::only($value, [ 'policy_types', 'categories', 'include_foundations', 'include_dependencies', ])); } $this->context = $context; } public function getFinishedAtAttribute(): mixed { return $this->completed_at; } public function setFinishedAtAttribute(mixed $value): void { $this->completed_at = $value; } public function isGovernanceArtifactOperation(): bool { return OperationCatalog::isGovernanceArtifactOperation((string) $this->type); } public function supportsOperatorExplanation(): bool { return OperationCatalog::supportsOperatorExplanation((string) $this->type); } public function governanceArtifactFamily(): ?string { return OperationCatalog::governanceArtifactFamily((string) $this->type); } /** * @return array */ public function artifactResultContext(): array { $context = is_array($this->context) ? $this->context : []; $result = is_array($context['result'] ?? null) ? $context['result'] : []; return array_merge($context, ['result' => $result]); } public function relatedArtifactId(): ?int { return match ($this->governanceArtifactFamily()) { 'baseline_snapshot' => is_numeric(data_get($this->context, 'result.snapshot_id')) ? (int) data_get($this->context, 'result.snapshot_id') : null, default => null, }; } /** * @return array */ public function reconciliation(): array { $context = is_array($this->context) ? $this->context : []; $reconciliation = $context['reconciliation'] ?? null; return is_array($reconciliation) ? $reconciliation : []; } public function isLifecycleReconciled(): bool { return $this->reconciliation() !== []; } public function lifecycleReconciliationReasonCode(): ?string { $reasonCode = $this->reconciliation()['reason_code'] ?? null; return is_string($reasonCode) && trim($reasonCode) !== '' ? trim($reasonCode) : null; } public function freshnessState(): OperationRunFreshnessState { return OperationRunFreshnessState::forRun($this); } /** * @return array */ public function baselineGapEnvelope(): array { $context = is_array($this->context) ? $this->context : []; return match ((string) $this->type) { 'baseline_compare' => is_array(data_get($context, 'baseline_compare.evidence_gaps')) ? data_get($context, 'baseline_compare.evidence_gaps') : [], 'baseline_capture' => is_array(data_get($context, 'baseline_capture.gaps')) ? data_get($context, 'baseline_capture.gaps') : [], default => [], }; } public function hasStructuredBaselineGapPayload(): bool { $subjects = $this->baselineGapEnvelope()['subjects'] ?? null; if (! is_array($subjects) || ! array_is_list($subjects) || $subjects === []) { return false; } foreach ($subjects as $subject) { if (! is_array($subject)) { return false; } foreach ([ 'policy_type', 'subject_key', 'subject_class', 'resolution_path', 'resolution_outcome', 'reason_code', 'operator_action_category', 'structural', 'retryable', ] as $key) { if (! array_key_exists($key, $subject)) { return false; } } } return true; } public function hasLegacyBaselineGapPayload(): bool { $envelope = $this->baselineGapEnvelope(); $byReason = is_array($envelope['by_reason'] ?? null) ? $envelope['by_reason'] : []; if (array_key_exists('policy_not_found', $byReason)) { return true; } $subjects = $envelope['subjects'] ?? null; if (! is_array($subjects)) { return false; } if (! array_is_list($subjects)) { return $subjects !== []; } if ($subjects === []) { return false; } return ! $this->hasStructuredBaselineGapPayload(); } }