TenantAtlas/apps/platform/app/Services/Evidence/EvidenceAnchorResolver.php
Ahmed Darrazi 75e9c30713
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 5m3s
feat: add evidence anchor reconciliation contracts and readiness fixes
2026-06-21 11:38:23 +02:00

424 lines
17 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Evidence;
use App\Filament\Resources\EvidenceSnapshotResource;
use App\Models\EnvironmentReview;
use App\Models\EvidenceSnapshot;
use App\Models\ManagedEnvironment;
use App\Models\ReviewPack;
use App\Models\User;
use App\Models\Workspace;
use App\Support\Auth\Capabilities;
use App\Support\Evidence\EvidenceCompletenessState;
use App\Support\Evidence\EvidenceSnapshotStatus;
use Illuminate\Database\Eloquent\Builder;
final class EvidenceAnchorResolver
{
public function currentForScope(Workspace $workspace, ?ManagedEnvironment $environment = null, ?User $actor = null): EvidenceAnchorResult
{
if (! $environment instanceof ManagedEnvironment) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_UNKNOWN,
primaryReason: 'Select an environment to open current evidence.',
blockingReasons: ['Workspace-wide evidence cannot be represented by one arbitrary environment snapshot.'],
displayLabel: 'Current evidence',
);
}
if ((int) $environment->workspace_id !== (int) $workspace->getKey()) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_BLOCKED,
primaryReason: 'The environment does not belong to the selected workspace.',
blockingReasons: ['Evidence scope mismatch.'],
displayLabel: 'Current evidence',
);
}
if ($actor instanceof User && ! $actor->canAccessTenant($environment)) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_BLOCKED,
primaryReason: 'You do not have access to this environment.',
blockingReasons: ['Environment access is required before evidence can be linked.'],
displayLabel: 'Current evidence',
);
}
$snapshot = $this->currentSnapshotForEnvironment($environment);
if (! $snapshot instanceof EvidenceSnapshot) {
$hasAnySnapshot = EvidenceSnapshot::query()
->where('workspace_id', (int) $workspace->getKey())
->where('managed_environment_id', (int) $environment->getKey())
->exists();
return $this->noValid(
state: $hasAnySnapshot
? EvidenceAnchorResult::STATE_NEEDS_ATTENTION
: EvidenceAnchorResult::STATE_NOT_CONFIGURED,
primaryReason: $hasAnySnapshot
? 'No complete active evidence snapshot is available for this environment.'
: 'No evidence snapshot is configured for this environment.',
blockingReasons: $hasAnySnapshot
? ['Only active, complete, non-expired evidence can be used as current evidence.']
: ['Generate evidence before opening a current evidence detail.'],
displayLabel: 'Current evidence',
);
}
return $this->fromSnapshot(
snapshot: $snapshot,
anchorType: EvidenceAnchorResult::TYPE_CURRENT_SCOPE_EVIDENCE,
actor: $actor,
displayLabel: 'Current evidence',
primaryReason: 'Current complete evidence is available for this environment.',
isCurrent: true,
);
}
public function currentSnapshotForEnvironment(ManagedEnvironment $environment): ?EvidenceSnapshot
{
$workspace = $environment->workspace;
if (! $workspace instanceof Workspace) {
return null;
}
return $this->currentSnapshotQuery($workspace, $environment)
->with([
'tenant',
'operationRun',
'reviewPacks.operationRun',
'reviewPacks.environmentReview.currentExportReviewPack',
'items',
])
->first();
}
public function currentSnapshotQuery(Workspace $workspace, ManagedEnvironment $environment): Builder
{
return EvidenceSnapshot::query()
->where('workspace_id', (int) $workspace->getKey())
->where('managed_environment_id', (int) $environment->getKey())
->where('status', EvidenceSnapshotStatus::Active->value)
->where('completeness_state', EvidenceCompletenessState::Complete->value)
->where(function (Builder $query): void {
$query
->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->orderByRaw('generated_at IS NULL')
->orderByDesc('generated_at')
->orderByDesc('id');
}
public function forReviewPackDraft(ReviewPack $reviewPack, ?User $actor = null): EvidenceAnchorResult
{
$reviewPack->loadMissing(['tenant', 'evidenceSnapshot']);
if (! $reviewPack->evidenceSnapshot instanceof EvidenceSnapshot) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_NOT_CONFIGURED,
primaryReason: 'This draft review pack has no bound evidence snapshot.',
blockingReasons: ['Draft review evidence is missing.'],
displayLabel: 'Draft review evidence',
);
}
return $this->fromSnapshot(
snapshot: $reviewPack->evidenceSnapshot,
anchorType: EvidenceAnchorResult::TYPE_REVIEW_DRAFT_EVIDENCE,
actor: $actor,
displayLabel: 'Draft review evidence',
primaryReason: 'Draft review evidence is bound to this pack.',
isTechnicalOnly: true,
);
}
public function forReviewPackRelease(ReviewPack $reviewPack, ?User $actor = null): EvidenceAnchorResult
{
$reviewPack->loadMissing(['tenant', 'evidenceSnapshot', 'environmentReview.evidenceSnapshot']);
$snapshot = $reviewPack->evidenceSnapshot
?? $reviewPack->environmentReview?->evidenceSnapshot;
if (! $snapshot instanceof EvidenceSnapshot) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_NOT_CONFIGURED,
primaryReason: 'This review pack has no release-bound evidence snapshot.',
blockingReasons: ['Release-bound evidence is missing.'],
displayLabel: 'Review evidence',
);
}
if (! $this->snapshotMatchesArtifactScope($snapshot, $reviewPack)) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_BLOCKED,
primaryReason: 'The bound evidence snapshot does not match the review pack scope.',
blockingReasons: ['Release-bound evidence scope mismatch.'],
displayLabel: 'Review evidence',
);
}
return $this->fromSnapshot(
snapshot: $snapshot,
anchorType: EvidenceAnchorResult::TYPE_REVIEW_RELEASED_EVIDENCE,
actor: $actor,
displayLabel: 'Review evidence',
primaryReason: 'Evidence captured for this released review is available.',
isReleaseBound: true,
);
}
public function forEnvironmentReviewRelease(EnvironmentReview $review, ?User $actor = null): EvidenceAnchorResult
{
$review->loadMissing(['tenant', 'evidenceSnapshot']);
if (! $review->evidenceSnapshot instanceof EvidenceSnapshot) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_NOT_CONFIGURED,
primaryReason: 'This review has no bound evidence snapshot.',
blockingReasons: ['Review evidence is missing.'],
displayLabel: 'Review evidence',
);
}
if (! $this->snapshotMatchesArtifactScope($review->evidenceSnapshot, $review)) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_BLOCKED,
primaryReason: 'The bound evidence snapshot does not match the review scope.',
blockingReasons: ['Review evidence scope mismatch.'],
displayLabel: 'Review evidence',
);
}
return $this->fromSnapshot(
snapshot: $review->evidenceSnapshot,
anchorType: EvidenceAnchorResult::TYPE_REVIEW_RELEASED_EVIDENCE,
actor: $actor,
displayLabel: 'Review evidence',
primaryReason: 'Evidence captured for this review is available.',
isReleaseBound: true,
);
}
public function forCustomerWorkspace(EnvironmentReview|ReviewPack $reviewPackOrReview, ?User $actor = null): EvidenceAnchorResult
{
$snapshot = match (true) {
$reviewPackOrReview instanceof EnvironmentReview => tap($reviewPackOrReview)->loadMissing('evidenceSnapshot')->evidenceSnapshot,
$reviewPackOrReview instanceof ReviewPack => tap($reviewPackOrReview)->loadMissing(['evidenceSnapshot', 'environmentReview.evidenceSnapshot'])->evidenceSnapshot
?? $reviewPackOrReview->environmentReview?->evidenceSnapshot,
};
if (! $snapshot instanceof EvidenceSnapshot) {
return $this->noValid(
anchorType: EvidenceAnchorResult::TYPE_CUSTOMER_SAFE_EVIDENCE_SUMMARY,
state: EvidenceAnchorResult::STATE_NOT_CONFIGURED,
primaryReason: 'No review evidence summary is available for this customer workspace item.',
blockingReasons: ['Released review evidence is missing.'],
displayLabel: 'Review evidence',
isCustomerSafe: true,
);
}
if (! $this->snapshotMatchesArtifactScope($snapshot, $reviewPackOrReview)) {
return $this->noValid(
state: EvidenceAnchorResult::STATE_BLOCKED,
primaryReason: 'Review evidence cannot be summarized because its scope does not match this customer workspace item.',
blockingReasons: ['Review evidence scope mismatch.'],
displayLabel: 'Review evidence',
);
}
$state = $this->stateForSnapshot($snapshot, releaseBound: true);
$isExpired = $this->isExpired($snapshot);
$isPartial = $this->isPartial($snapshot);
return new EvidenceAnchorResult(
anchorType: EvidenceAnchorResult::TYPE_CUSTOMER_SAFE_EVIDENCE_SUMMARY,
state: $state,
evidenceSnapshotId: null,
targetRoute: null,
isCurrent: false,
isReleaseBound: true,
isCustomerSafe: true,
isTechnicalOnly: false,
isPartial: $isPartial,
isSuperseded: (string) $snapshot->status === EvidenceSnapshotStatus::Superseded->value,
isExpired: $isExpired,
canLink: false,
canViewTechnicalDetail: false,
primaryReason: $isExpired
? 'The evidence captured for this review is expired.'
: 'Evidence captured for this review is summarized for customer review.',
blockingReasons: $state === EvidenceAnchorResult::STATE_READY ? [] : [$this->blockingReasonForSnapshot($snapshot, true)],
displayLabel: 'Evidence captured for this review',
);
}
public function technicalDetail(EvidenceSnapshot $snapshot, ?User $actor = null): EvidenceAnchorResult
{
$snapshot->loadMissing('tenant');
$targetRoute = $this->targetRouteFor($snapshot, $actor);
return $this->fromSnapshot(
snapshot: $snapshot,
anchorType: EvidenceAnchorResult::TYPE_TECHNICAL_EVIDENCE_DETAIL,
actor: $actor,
displayLabel: 'View internal evidence details',
primaryReason: $targetRoute !== null
? 'Internal evidence details are available for authorized operators.'
: 'Internal evidence details require evidence view permission.',
isTechnicalOnly: true,
targetRoute: $targetRoute,
);
}
private function fromSnapshot(
EvidenceSnapshot $snapshot,
string $anchorType,
?User $actor,
string $displayLabel,
string $primaryReason,
bool $isCurrent = false,
bool $isReleaseBound = false,
bool $isCustomerSafe = false,
bool $isTechnicalOnly = false,
?string $targetRoute = null,
): EvidenceAnchorResult {
$snapshot->loadMissing('tenant');
$targetRoute ??= $this->targetRouteFor($snapshot, $actor);
$state = $this->stateForSnapshot($snapshot, $isReleaseBound);
$canViewTechnicalDetail = $this->targetRouteFor($snapshot, $actor) !== null;
return new EvidenceAnchorResult(
anchorType: $anchorType,
state: $state,
evidenceSnapshotId: (int) $snapshot->getKey(),
targetRoute: $targetRoute,
isCurrent: $isCurrent,
isReleaseBound: $isReleaseBound,
isCustomerSafe: $isCustomerSafe,
isTechnicalOnly: $isTechnicalOnly,
isPartial: $this->isPartial($snapshot),
isSuperseded: (string) $snapshot->status === EvidenceSnapshotStatus::Superseded->value,
isExpired: $this->isExpired($snapshot),
canLink: $targetRoute !== null,
canViewTechnicalDetail: $canViewTechnicalDetail,
primaryReason: $primaryReason,
blockingReasons: $state === EvidenceAnchorResult::STATE_READY
? []
: [$this->blockingReasonForSnapshot($snapshot, $isReleaseBound)],
displayLabel: $displayLabel,
);
}
/**
* @param list<string> $blockingReasons
*/
private function noValid(
string $state,
string $primaryReason,
array $blockingReasons,
string $displayLabel,
string $anchorType = EvidenceAnchorResult::TYPE_NO_VALID_EVIDENCE,
bool $isCustomerSafe = false,
): EvidenceAnchorResult {
return new EvidenceAnchorResult(
anchorType: $anchorType,
state: $state,
evidenceSnapshotId: null,
targetRoute: null,
isCurrent: false,
isReleaseBound: false,
isCustomerSafe: $isCustomerSafe,
isTechnicalOnly: false,
isPartial: false,
isSuperseded: false,
isExpired: false,
canLink: false,
canViewTechnicalDetail: false,
primaryReason: $primaryReason,
blockingReasons: $blockingReasons,
displayLabel: $displayLabel,
);
}
private function targetRouteFor(EvidenceSnapshot $snapshot, ?User $actor): ?string
{
$tenant = $snapshot->tenant;
if (! $actor instanceof User || ! $tenant instanceof ManagedEnvironment) {
return null;
}
if (! $actor->canAccessTenant($tenant) || ! $actor->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
return null;
}
return EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'admin');
}
private function stateForSnapshot(EvidenceSnapshot $snapshot, bool $releaseBound): string
{
if ($this->isExpired($snapshot)) {
return EvidenceAnchorResult::STATE_EXPIRED;
}
if ((string) $snapshot->status === EvidenceSnapshotStatus::Failed->value) {
return EvidenceAnchorResult::STATE_BLOCKED;
}
if ($this->isPartial($snapshot)) {
return EvidenceAnchorResult::STATE_NEEDS_ATTENTION;
}
if (! $releaseBound && (string) $snapshot->status !== EvidenceSnapshotStatus::Active->value) {
return EvidenceAnchorResult::STATE_BLOCKED;
}
return EvidenceAnchorResult::STATE_READY;
}
private function blockingReasonForSnapshot(EvidenceSnapshot $snapshot, bool $releaseBound): string
{
if ($this->isExpired($snapshot)) {
return 'Evidence has expired.';
}
if ((string) $snapshot->status === EvidenceSnapshotStatus::Failed->value) {
return 'Evidence generation failed.';
}
if ($this->isPartial($snapshot)) {
return 'Evidence completeness requires attention.';
}
if (! $releaseBound && (string) $snapshot->status !== EvidenceSnapshotStatus::Active->value) {
return 'Evidence is not an active current snapshot.';
}
return 'Evidence cannot be linked from this surface.';
}
private function isPartial(EvidenceSnapshot $snapshot): bool
{
return (string) $snapshot->completeness_state !== EvidenceCompletenessState::Complete->value;
}
private function isExpired(EvidenceSnapshot $snapshot): bool
{
return (string) $snapshot->status === EvidenceSnapshotStatus::Expired->value
|| ($snapshot->expires_at !== null && $snapshot->expires_at->isPast());
}
private function snapshotMatchesArtifactScope(EvidenceSnapshot $snapshot, EnvironmentReview|ReviewPack $artifact): bool
{
return (int) $snapshot->workspace_id === (int) $artifact->workspace_id
&& (int) $snapshot->managed_environment_id === (int) $artifact->managed_environment_id;
}
}