supportsType((string) $run->type)) { return ReconciliationResult::unsupported( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'This adapter only supports evidence snapshot generation runs.', evidence: [ 'adapter' => $this->key(), 'type' => (string) $run->type, ], ); } $context = is_array($run->context) ? $run->context : []; $workspaceId = (int) ($run->workspace_id ?? $context['workspace_id'] ?? 0); $tenantId = (int) ($run->managed_environment_id ?? $context['managed_environment_id'] ?? 0); $fingerprint = is_string($context['fingerprint'] ?? null) ? trim((string) $context['fingerprint']) : ''; $evidence = [ 'adapter' => $this->key(), 'operation_run_id' => (int) $run->getKey(), 'workspace_id' => $workspaceId, 'managed_environment_id' => $tenantId, 'fingerprint' => $fingerprint, ]; if ($workspaceId <= 0 || $tenantId <= 0 || $fingerprint === '') { return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'The run no longer carries enough evidence snapshot scope to reconcile it safely.', evidence: $evidence, ); } $snapshot = EvidenceSnapshot::query() ->where('workspace_id', $workspaceId) ->where('managed_environment_id', $tenantId) ->where('fingerprint', $fingerprint) ->latest('id') ->first(); if (! $snapshot instanceof EvidenceSnapshot) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: 'No matching evidence snapshot is available yet for this run.', evidence: $evidence, ); } $related = $this->relatedSnapshotMetadata($snapshot); $status = (string) $snapshot->status; $completeness = (string) $snapshot->completeness_state; if ($this->isUsableSnapshot($snapshot)) { return ReconciliationResult::reconciledSucceeded( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: (int) ($snapshot->operation_run_id ?? 0) === (int) $run->getKey() ? 'The queued evidence snapshot was already completed before the run finished updating.' : 'A matching evidence snapshot was already available for this run.', evidence: $evidence + ['chosen_snapshot_id' => (int) $snapshot->getKey()], related: $related, summaryCounts: $this->summaryCounts($snapshot), ); } if (in_array($status, [ EvidenceSnapshotStatus::Queued->value, EvidenceSnapshotStatus::Generating->value, ], true)) { return ReconciliationResult::notReconciled( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: (int) ($snapshot->operation_run_id ?? 0) === (int) $run->getKey() ? 'The queued evidence snapshot is still generating and does not prove final snapshot truth yet.' : 'A matching evidence snapshot exists, but it is still generating.', evidence: $evidence + ['chosen_snapshot_id' => (int) $snapshot->getKey()], related: $related, ); } $reasonMessage = match (true) { $status === EvidenceSnapshotStatus::Expired->value => 'A matching evidence snapshot exists, but it is already expired.', $status === EvidenceSnapshotStatus::Superseded->value => 'A matching evidence snapshot exists, but it was superseded by newer evidence.', $status === EvidenceSnapshotStatus::Failed->value => 'A matching evidence snapshot exists, but evidence generation failed.', $completeness === EvidenceCompletenessState::Stale->value => 'A matching evidence snapshot exists, but its evidence basis is already stale.', $completeness === EvidenceCompletenessState::Partial->value => 'A matching evidence snapshot exists, but its evidence basis is incomplete.', default => 'A matching evidence snapshot exists, but it does not provide usable evidence truth yet.', }; return ReconciliationResult::attentionRequired( reasonCode: LifecycleReconciliationReason::AdapterOutOfSync->value, reasonMessage: $reasonMessage, evidence: $evidence + ['chosen_snapshot_id' => (int) $snapshot->getKey()], related: $related, summaryCounts: $this->summaryCounts($snapshot), ); } public function reconcileException(OperationRun $run, Throwable $throwable): ?ReconciliationResult { return null; } private function isUsableSnapshot(EvidenceSnapshot $snapshot): bool { if ((string) $snapshot->status !== EvidenceSnapshotStatus::Active->value) { return false; } if ((string) $snapshot->completeness_state !== EvidenceCompletenessState::Complete->value) { return false; } return $snapshot->expires_at === null || $snapshot->expires_at->isFuture(); } /** * @return array */ private function relatedSnapshotMetadata(EvidenceSnapshot $snapshot): array { return array_filter([ 'type' => 'evidence_snapshot', 'id' => (int) $snapshot->getKey(), 'status' => (string) $snapshot->status, 'completeness_state' => (string) $snapshot->completeness_state, 'fingerprint' => (string) $snapshot->fingerprint, 'operation_run_id' => is_numeric($snapshot->operation_run_id) ? (int) $snapshot->operation_run_id : null, 'expires_at' => $snapshot->expires_at?->toIso8601String(), ], static fn (mixed $value): bool => $value !== null && $value !== []); } /** * @return array */ private function summaryCounts(EvidenceSnapshot $snapshot): array { $summary = is_array($snapshot->summary) ? $snapshot->summary : []; return array_filter([ 'finding_count' => is_numeric($summary['finding_count'] ?? null) ? (int) $summary['finding_count'] : null, 'report_count' => is_numeric($summary['report_count'] ?? null) ? (int) $summary['report_count'] : null, 'operation_count' => is_numeric($summary['operation_count'] ?? null) ? (int) $summary['operation_count'] : null, ], static fn (mixed $value): bool => is_int($value)); } }