774 lines
37 KiB
PHP
774 lines
37 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Ui\GovernanceArtifactTruth;
|
|
|
|
use App\Filament\Resources\BaselineSnapshotResource;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Filament\Resources\TenantReviewResource;
|
|
use App\Models\BaselineSnapshot;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\TenantReview;
|
|
use App\Services\Baselines\SnapshotRendering\FidelityState;
|
|
use App\Support\Badges\BadgeCatalog;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\OperationCatalog;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
|
use App\Support\ReasonTranslation\ReasonResolutionEnvelope;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\TenantReviewCompletenessState;
|
|
use App\Support\TenantReviewStatus;
|
|
use Illuminate\Support\Arr;
|
|
|
|
final class ArtifactTruthPresenter
|
|
{
|
|
public function __construct(
|
|
private readonly ReasonPresenter $reasonPresenter,
|
|
) {}
|
|
|
|
public function for(mixed $record): ?ArtifactTruthEnvelope
|
|
{
|
|
return match (true) {
|
|
$record instanceof BaselineSnapshot => $this->forBaselineSnapshot($record),
|
|
$record instanceof EvidenceSnapshot => $this->forEvidenceSnapshot($record),
|
|
$record instanceof TenantReview => $this->forTenantReview($record),
|
|
$record instanceof ReviewPack => $this->forReviewPack($record),
|
|
$record instanceof OperationRun => $this->forOperationRun($record),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
public function forBaselineSnapshot(BaselineSnapshot $snapshot): ArtifactTruthEnvelope
|
|
{
|
|
$snapshot->loadMissing('baselineProfile');
|
|
|
|
$summary = is_array($snapshot->summary_jsonb) ? $snapshot->summary_jsonb : [];
|
|
$hasItems = (int) ($summary['total_items'] ?? 0) > 0;
|
|
$fidelity = FidelityState::fromSummary($summary, $hasItems);
|
|
$isHistorical = (int) ($snapshot->baselineProfile?->active_snapshot_id ?? 0) !== (int) $snapshot->getKey()
|
|
&& $snapshot->baselineProfile !== null;
|
|
$gapReasons = is_array(Arr::get($summary, 'gaps.by_reason')) ? Arr::get($summary, 'gaps.by_reason') : [];
|
|
$severeGapReasons = array_filter(
|
|
$gapReasons,
|
|
static fn (mixed $count, string $reason): bool => is_numeric($count) && (int) $count > 0 && $reason !== 'meta_fallback',
|
|
ARRAY_FILTER_USE_BOTH,
|
|
);
|
|
$reasonCode = $this->firstReasonCode($severeGapReasons);
|
|
$reason = $this->reasonPresenter->forArtifactTruth($reasonCode, 'artifact_truth');
|
|
|
|
$artifactExistence = match (true) {
|
|
$isHistorical => 'historical_only',
|
|
! $hasItems => 'created_but_not_usable',
|
|
default => 'created',
|
|
};
|
|
|
|
$contentState = match ($fidelity) {
|
|
FidelityState::Full => $severeGapReasons === [] ? 'trusted' : 'partial',
|
|
FidelityState::Partial => 'partial',
|
|
FidelityState::ReferenceOnly => 'reference_only',
|
|
FidelityState::Unsupported => $hasItems ? 'unsupported' : 'empty',
|
|
};
|
|
|
|
if (! $hasItems && $reasonCode !== null) {
|
|
$contentState = 'missing_input';
|
|
}
|
|
|
|
$freshnessState = $isHistorical ? 'stale' : 'current';
|
|
$supportState = in_array($contentState, ['reference_only', 'unsupported'], true) ? 'limited_support' : 'normal';
|
|
$actionability = match (true) {
|
|
$artifactExistence === 'historical_only' => 'none',
|
|
$contentState === 'trusted' && $freshnessState === 'current' => 'none',
|
|
$freshnessState === 'stale' => 'optional',
|
|
in_array($contentState, ['reference_only', 'unsupported'], true) => 'optional',
|
|
default => 'required',
|
|
};
|
|
|
|
[$primaryDomain, $primaryState, $primaryExplanation, $diagnosticLabel] = match (true) {
|
|
$artifactExistence === 'historical_only' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'historical_only',
|
|
'This snapshot remains readable for historical comparison, but it is not the current baseline artifact.',
|
|
$supportState === 'limited_support' ? 'Support limited' : null,
|
|
],
|
|
$artifactExistence === 'created_but_not_usable' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'created_but_not_usable',
|
|
$reason?->shortExplanation ?? 'A snapshot row exists, but it does not contain a trustworthy baseline artifact yet.',
|
|
$supportState === 'limited_support' ? 'Support limited' : null,
|
|
],
|
|
$contentState !== 'trusted' => [
|
|
BadgeDomain::GovernanceArtifactContent,
|
|
$contentState,
|
|
$reason?->shortExplanation ?? $this->contentExplanation($contentState),
|
|
$supportState === 'limited_support' ? 'Support limited' : null,
|
|
],
|
|
default => [
|
|
BadgeDomain::GovernanceArtifactContent,
|
|
'trusted',
|
|
'Structured capture content is available for this baseline snapshot.',
|
|
null,
|
|
],
|
|
};
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'baseline_snapshot',
|
|
artifactKey: 'baseline_snapshot:'.$snapshot->getKey(),
|
|
workspaceId: (int) $snapshot->workspace_id,
|
|
tenantId: null,
|
|
executionOutcome: null,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: $freshnessState,
|
|
publicationReadiness: null,
|
|
supportState: $supportState,
|
|
actionability: $actionability,
|
|
primaryDomain: $primaryDomain,
|
|
primaryState: $primaryState,
|
|
primaryExplanation: $primaryExplanation,
|
|
diagnosticLabel: $diagnosticLabel,
|
|
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
|
|
nextActionLabel: $this->nextActionLabel(
|
|
$actionability,
|
|
$reason,
|
|
match ($actionability) {
|
|
'required' => 'Inspect the related capture diagnostics before using this snapshot',
|
|
'optional' => 'Review the capture diagnostics before comparing this snapshot',
|
|
default => null,
|
|
},
|
|
),
|
|
nextActionUrl: null,
|
|
relatedRunId: null,
|
|
relatedArtifactUrl: BaselineSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin'),
|
|
includePublicationDimension: false,
|
|
);
|
|
}
|
|
|
|
public function forEvidenceSnapshot(EvidenceSnapshot $snapshot): ArtifactTruthEnvelope
|
|
{
|
|
$snapshot->loadMissing('tenant');
|
|
|
|
$summary = is_array($snapshot->summary) ? $snapshot->summary : [];
|
|
$missingDimensions = (int) ($summary['missing_dimensions'] ?? 0);
|
|
$staleDimensions = (int) ($summary['stale_dimensions'] ?? 0);
|
|
$status = (string) $snapshot->status;
|
|
|
|
$artifactExistence = match ($status) {
|
|
'queued', 'generating' => 'not_created',
|
|
'expired', 'superseded' => 'historical_only',
|
|
'failed' => 'created_but_not_usable',
|
|
default => 'created',
|
|
};
|
|
|
|
$contentState = match (true) {
|
|
$artifactExistence === 'not_created' => 'missing_input',
|
|
$artifactExistence === 'historical_only' && $snapshot->completeness_state === 'missing' => 'empty',
|
|
$status === 'failed' => 'missing_input',
|
|
$snapshot->completeness_state === 'missing' => 'missing_input',
|
|
$snapshot->completeness_state === 'partial' => 'partial',
|
|
default => 'trusted',
|
|
};
|
|
|
|
if ((int) ($summary['dimension_count'] ?? 0) === 0 && $artifactExistence !== 'not_created') {
|
|
$contentState = 'empty';
|
|
}
|
|
|
|
$freshnessState = match (true) {
|
|
$artifactExistence === 'historical_only' => 'stale',
|
|
$snapshot->completeness_state === 'stale' || $staleDimensions > 0 => 'stale',
|
|
in_array($status, ['queued', 'generating'], true) => 'unknown',
|
|
default => 'current',
|
|
};
|
|
|
|
$actionability = match (true) {
|
|
$artifactExistence === 'historical_only' => 'none',
|
|
in_array($status, ['queued', 'generating'], true) => 'optional',
|
|
$contentState === 'trusted' && $freshnessState === 'current' => 'none',
|
|
$freshnessState === 'stale' => 'optional',
|
|
default => 'required',
|
|
};
|
|
|
|
$reasonCode = match (true) {
|
|
$status === 'failed' => 'evidence_generation_failed',
|
|
$missingDimensions > 0 => 'evidence_missing_dimensions',
|
|
$staleDimensions > 0 => 'evidence_stale_dimensions',
|
|
default => null,
|
|
};
|
|
$reason = $this->reasonPresenter->forArtifactTruth($reasonCode, 'artifact_truth');
|
|
|
|
[$primaryDomain, $primaryState, $primaryExplanation] = match (true) {
|
|
$artifactExistence === 'not_created' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'not_created',
|
|
'The evidence generation request exists, but no tenant snapshot is available yet.',
|
|
],
|
|
$artifactExistence === 'historical_only' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'historical_only',
|
|
'This evidence snapshot remains available for history, but it is not the current working evidence artifact.',
|
|
],
|
|
$contentState !== 'trusted' => [
|
|
BadgeDomain::GovernanceArtifactContent,
|
|
$contentState,
|
|
$reason?->shortExplanation ?? $this->contentExplanation($contentState),
|
|
],
|
|
$freshnessState === 'stale' => [
|
|
BadgeDomain::GovernanceArtifactFreshness,
|
|
'stale',
|
|
$reason?->shortExplanation ?? 'The snapshot exists, but one or more evidence dimensions should be refreshed before relying on it.',
|
|
],
|
|
default => [
|
|
BadgeDomain::GovernanceArtifactContent,
|
|
'trusted',
|
|
'A current evidence snapshot is available for review work.',
|
|
],
|
|
};
|
|
|
|
$nextActionUrl = $snapshot->operation_run_id
|
|
? OperationRunLinks::tenantlessView((int) $snapshot->operation_run_id)
|
|
: null;
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'evidence_snapshot',
|
|
artifactKey: 'evidence_snapshot:'.$snapshot->getKey(),
|
|
workspaceId: (int) $snapshot->workspace_id,
|
|
tenantId: $snapshot->tenant_id !== null ? (int) $snapshot->tenant_id : null,
|
|
executionOutcome: null,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: $freshnessState,
|
|
publicationReadiness: null,
|
|
supportState: 'normal',
|
|
actionability: $actionability,
|
|
primaryDomain: $primaryDomain,
|
|
primaryState: $primaryState,
|
|
primaryExplanation: $primaryExplanation,
|
|
diagnosticLabel: $missingDimensions > 0 && $staleDimensions > 0
|
|
? sprintf('%d missing, %d stale dimensions', $missingDimensions, $staleDimensions)
|
|
: null,
|
|
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
|
|
nextActionLabel: $this->nextActionLabel(
|
|
$actionability,
|
|
$reason,
|
|
match ($actionability) {
|
|
'required' => 'Refresh evidence before using this snapshot',
|
|
'optional' => in_array($status, ['queued', 'generating'], true)
|
|
? 'Wait for evidence generation to finish'
|
|
: 'Review the evidence freshness before relying on this snapshot',
|
|
default => null,
|
|
},
|
|
),
|
|
nextActionUrl: $nextActionUrl,
|
|
relatedRunId: $snapshot->operation_run_id !== null ? (int) $snapshot->operation_run_id : null,
|
|
relatedArtifactUrl: $snapshot->tenant !== null
|
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant)
|
|
: null,
|
|
includePublicationDimension: false,
|
|
);
|
|
}
|
|
|
|
public function forTenantReview(TenantReview $review): ArtifactTruthEnvelope
|
|
{
|
|
$review->loadMissing(['tenant', 'currentExportReviewPack']);
|
|
|
|
$summary = is_array($review->summary) ? $review->summary : [];
|
|
$publishBlockers = $review->publishBlockers();
|
|
$status = $review->statusEnum();
|
|
$completeness = $review->completenessEnum()->value;
|
|
$sectionCounts = is_array($summary['section_state_counts'] ?? null) ? $summary['section_state_counts'] : [];
|
|
$staleSections = (int) ($sectionCounts['stale'] ?? 0);
|
|
|
|
$artifactExistence = match ($status) {
|
|
TenantReviewStatus::Archived, TenantReviewStatus::Superseded => 'historical_only',
|
|
TenantReviewStatus::Failed => 'created_but_not_usable',
|
|
default => 'created',
|
|
};
|
|
|
|
$contentState = match ($completeness) {
|
|
TenantReviewCompletenessState::Complete->value => 'trusted',
|
|
TenantReviewCompletenessState::Partial->value => 'partial',
|
|
TenantReviewCompletenessState::Missing->value => 'missing_input',
|
|
TenantReviewCompletenessState::Stale->value => 'trusted',
|
|
default => 'partial',
|
|
};
|
|
|
|
$freshnessState = match (true) {
|
|
$artifactExistence === 'historical_only' => 'stale',
|
|
$completeness === TenantReviewCompletenessState::Stale->value || $staleSections > 0 => 'stale',
|
|
default => 'current',
|
|
};
|
|
|
|
$publicationReadiness = match (true) {
|
|
$artifactExistence === 'historical_only' => 'internal_only',
|
|
$status === TenantReviewStatus::Published => 'publishable',
|
|
$publishBlockers !== [] => 'blocked',
|
|
$status === TenantReviewStatus::Ready => 'publishable',
|
|
default => 'internal_only',
|
|
};
|
|
|
|
$actionability = match (true) {
|
|
$artifactExistence === 'historical_only' => 'none',
|
|
$publicationReadiness === 'publishable' && $freshnessState === 'current' => 'none',
|
|
$publicationReadiness === 'internal_only' && $contentState === 'trusted' => 'optional',
|
|
$freshnessState === 'stale' && $publishBlockers === [] => 'optional',
|
|
default => 'required',
|
|
};
|
|
|
|
$reasonCode = match (true) {
|
|
$publishBlockers !== [] => 'review_publish_blocked',
|
|
$status === TenantReviewStatus::Failed => 'review_generation_failed',
|
|
$completeness === TenantReviewCompletenessState::Missing->value => 'review_missing_sections',
|
|
$completeness === TenantReviewCompletenessState::Stale->value => 'review_stale_sections',
|
|
default => null,
|
|
};
|
|
$reason = $this->reasonPresenter->forArtifactTruth($reasonCode, 'artifact_truth');
|
|
|
|
[$primaryDomain, $primaryState, $primaryExplanation] = match (true) {
|
|
$artifactExistence === 'historical_only' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'historical_only',
|
|
'This review remains available as historical evidence, but it is no longer the current review artifact.',
|
|
],
|
|
$publicationReadiness === 'blocked' => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'blocked',
|
|
$publishBlockers[0] ?? $reason?->shortExplanation ?? 'This review exists, but it is blocked from publication or export.',
|
|
],
|
|
$publicationReadiness === 'internal_only' => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'internal_only',
|
|
'This review exists and is useful internally, but it is not yet ready for stakeholder publication.',
|
|
],
|
|
$freshnessState === 'stale' => [
|
|
BadgeDomain::GovernanceArtifactFreshness,
|
|
'stale',
|
|
$reason?->shortExplanation ?? 'The review exists, but one or more required sections should be refreshed before publication.',
|
|
],
|
|
default => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'publishable',
|
|
'This review is ready for publication and executive-pack export.',
|
|
],
|
|
};
|
|
|
|
$nextActionUrl = $review->operation_run_id
|
|
? OperationRunLinks::tenantlessView((int) $review->operation_run_id)
|
|
: null;
|
|
|
|
if ($publishBlockers !== [] && $review->tenant !== null) {
|
|
$nextActionUrl = TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant);
|
|
}
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'tenant_review',
|
|
artifactKey: 'tenant_review:'.$review->getKey(),
|
|
workspaceId: (int) $review->workspace_id,
|
|
tenantId: $review->tenant_id !== null ? (int) $review->tenant_id : null,
|
|
executionOutcome: null,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: $freshnessState,
|
|
publicationReadiness: $publicationReadiness,
|
|
supportState: 'normal',
|
|
actionability: $actionability,
|
|
primaryDomain: $primaryDomain,
|
|
primaryState: $primaryState,
|
|
primaryExplanation: $primaryExplanation,
|
|
diagnosticLabel: $contentState !== 'trusted'
|
|
? BadgeCatalog::spec(BadgeDomain::GovernanceArtifactContent, $contentState)->label
|
|
: null,
|
|
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
|
|
nextActionLabel: $this->nextActionLabel(
|
|
$actionability,
|
|
$reason,
|
|
match ($actionability) {
|
|
'required' => 'Resolve the review blockers before publication',
|
|
'optional' => 'Complete the remaining review work before publication',
|
|
default => null,
|
|
},
|
|
),
|
|
nextActionUrl: $nextActionUrl,
|
|
relatedRunId: $review->operation_run_id !== null ? (int) $review->operation_run_id : null,
|
|
relatedArtifactUrl: $review->tenant !== null
|
|
? TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant)
|
|
: null,
|
|
includePublicationDimension: true,
|
|
);
|
|
}
|
|
|
|
public function forReviewPack(ReviewPack $pack): ArtifactTruthEnvelope
|
|
{
|
|
$pack->loadMissing(['tenant', 'tenantReview']);
|
|
|
|
$summary = is_array($pack->summary) ? $pack->summary : [];
|
|
$status = (string) $pack->status;
|
|
$evidenceResolution = is_array($summary['evidence_resolution'] ?? null) ? $summary['evidence_resolution'] : [];
|
|
$sourceReview = $pack->tenantReview;
|
|
$sourceBlockers = $sourceReview instanceof TenantReview ? $sourceReview->publishBlockers() : [];
|
|
$sourceReviewStatus = $sourceReview instanceof TenantReview ? $sourceReview->statusEnum() : null;
|
|
|
|
$artifactExistence = match ($status) {
|
|
ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value => 'not_created',
|
|
ReviewPackStatus::Expired->value => 'historical_only',
|
|
ReviewPackStatus::Failed->value => 'created_but_not_usable',
|
|
default => 'created',
|
|
};
|
|
|
|
$contentState = match (true) {
|
|
$artifactExistence === 'not_created' => 'missing_input',
|
|
$status === ReviewPackStatus::Failed->value => 'missing_input',
|
|
($evidenceResolution['outcome'] ?? null) !== 'resolved' => 'missing_input',
|
|
$sourceReview instanceof TenantReview && $sourceBlockers !== [] => 'partial',
|
|
default => 'trusted',
|
|
};
|
|
|
|
$freshnessState = $artifactExistence === 'historical_only' ? 'stale' : 'current';
|
|
$publicationReadiness = match (true) {
|
|
$artifactExistence === 'historical_only' => 'internal_only',
|
|
$artifactExistence === 'not_created' => 'blocked',
|
|
$status === ReviewPackStatus::Failed->value => 'blocked',
|
|
$sourceReview instanceof TenantReview && $sourceBlockers !== [] => 'blocked',
|
|
$sourceReviewStatus === TenantReviewStatus::Draft || $sourceReviewStatus === TenantReviewStatus::Failed => 'internal_only',
|
|
default => $status === ReviewPackStatus::Ready->value ? 'publishable' : 'blocked',
|
|
};
|
|
|
|
$actionability = match (true) {
|
|
$artifactExistence === 'historical_only' => 'none',
|
|
$publicationReadiness === 'publishable' => 'none',
|
|
$publicationReadiness === 'internal_only' => 'optional',
|
|
default => 'required',
|
|
};
|
|
|
|
$reasonCode = match (true) {
|
|
$status === ReviewPackStatus::Failed->value => 'review_pack_generation_failed',
|
|
($evidenceResolution['outcome'] ?? null) !== 'resolved' => 'review_pack_missing_snapshot',
|
|
$sourceReview instanceof TenantReview && $sourceBlockers !== [] => 'review_pack_source_not_publishable',
|
|
$artifactExistence === 'historical_only' => 'review_pack_expired',
|
|
default => null,
|
|
};
|
|
$reason = $this->reasonPresenter->forArtifactTruth($reasonCode, 'artifact_truth');
|
|
|
|
[$primaryDomain, $primaryState, $primaryExplanation] = match (true) {
|
|
$artifactExistence === 'historical_only' => [
|
|
BadgeDomain::GovernanceArtifactExistence,
|
|
'historical_only',
|
|
'This pack remains available as a historical export, but it is no longer the current stakeholder artifact.',
|
|
],
|
|
$publicationReadiness === 'blocked' => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'blocked',
|
|
$sourceBlockers[0] ?? $reason?->shortExplanation ?? 'A pack file is not yet available for trustworthy stakeholder delivery.',
|
|
],
|
|
$publicationReadiness === 'internal_only' => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'internal_only',
|
|
'This pack can be reviewed internally, but the source review is not currently publishable.',
|
|
],
|
|
default => [
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
'publishable',
|
|
'This executive pack is ready for stakeholder delivery.',
|
|
],
|
|
};
|
|
|
|
$nextActionUrl = null;
|
|
|
|
if ($sourceReview instanceof TenantReview && $pack->tenant !== null) {
|
|
$nextActionUrl = TenantReviewResource::tenantScopedUrl('view', ['record' => $sourceReview], $pack->tenant);
|
|
} elseif ($pack->operation_run_id !== null) {
|
|
$nextActionUrl = OperationRunLinks::tenantlessView((int) $pack->operation_run_id);
|
|
}
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'review_pack',
|
|
artifactKey: 'review_pack:'.$pack->getKey(),
|
|
workspaceId: (int) $pack->workspace_id,
|
|
tenantId: $pack->tenant_id !== null ? (int) $pack->tenant_id : null,
|
|
executionOutcome: null,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: $freshnessState,
|
|
publicationReadiness: $publicationReadiness,
|
|
supportState: 'normal',
|
|
actionability: $actionability,
|
|
primaryDomain: $primaryDomain,
|
|
primaryState: $primaryState,
|
|
primaryExplanation: $primaryExplanation,
|
|
diagnosticLabel: $contentState !== 'trusted'
|
|
? BadgeCatalog::spec(BadgeDomain::GovernanceArtifactContent, $contentState)->label
|
|
: null,
|
|
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
|
|
nextActionLabel: $this->nextActionLabel(
|
|
$actionability,
|
|
$reason,
|
|
match ($actionability) {
|
|
'required' => 'Open the source review before sharing this pack',
|
|
'optional' => 'Review the source review before sharing this pack',
|
|
default => null,
|
|
},
|
|
),
|
|
nextActionUrl: $nextActionUrl,
|
|
relatedRunId: $pack->operation_run_id !== null ? (int) $pack->operation_run_id : null,
|
|
relatedArtifactUrl: $pack->tenant !== null
|
|
? ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $pack->tenant)
|
|
: null,
|
|
includePublicationDimension: true,
|
|
);
|
|
}
|
|
|
|
public function forOperationRun(OperationRun $run): ArtifactTruthEnvelope
|
|
{
|
|
$artifact = $this->resolveArtifactForRun($run);
|
|
$reason = $this->reasonPresenter->forOperationRun($run, 'run_detail');
|
|
|
|
if ($artifact !== null) {
|
|
$artifactEnvelope = $this->for($artifact);
|
|
|
|
if ($artifactEnvelope instanceof ArtifactTruthEnvelope) {
|
|
$diagnosticParts = array_values(array_filter([
|
|
BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label !== 'Unknown'
|
|
? BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label
|
|
: null,
|
|
$artifactEnvelope->diagnosticLabel,
|
|
]));
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'artifact_run',
|
|
artifactKey: 'artifact_run:'.$run->getKey(),
|
|
workspaceId: (int) $run->workspace_id,
|
|
tenantId: $run->tenant_id !== null ? (int) $run->tenant_id : null,
|
|
executionOutcome: $run->outcome !== null ? (string) $run->outcome : null,
|
|
artifactExistence: $artifactEnvelope->artifactExistence,
|
|
contentState: $artifactEnvelope->contentState,
|
|
freshnessState: $artifactEnvelope->freshnessState,
|
|
publicationReadiness: $artifactEnvelope->publicationReadiness,
|
|
supportState: $artifactEnvelope->supportState,
|
|
actionability: $artifactEnvelope->actionability,
|
|
primaryDomain: $artifactEnvelope->primaryDimension()?->badgeDomain ?? BadgeDomain::GovernanceArtifactExistence,
|
|
primaryState: $artifactEnvelope->primaryDimension()?->badgeState ?? $artifactEnvelope->artifactExistence,
|
|
primaryExplanation: $artifactEnvelope->primaryExplanation ?? $reason?->shortExplanation ?? 'The run finished, but the related artifact needs review.',
|
|
diagnosticLabel: $diagnosticParts === [] ? null : implode(' · ', $diagnosticParts),
|
|
reason: $artifactEnvelope->reason,
|
|
nextActionLabel: $artifactEnvelope->nextActionLabel,
|
|
nextActionUrl: $artifactEnvelope->relatedArtifactUrl ?? $artifactEnvelope->nextActionUrl,
|
|
relatedRunId: (int) $run->getKey(),
|
|
relatedArtifactUrl: $artifactEnvelope->relatedArtifactUrl,
|
|
includePublicationDimension: $artifactEnvelope->publicationReadiness !== null,
|
|
);
|
|
}
|
|
}
|
|
|
|
$artifactExistence = match ((string) $run->status) {
|
|
OperationRunStatus::Queued->value, OperationRunStatus::Running->value => 'not_created',
|
|
default => 'not_created',
|
|
};
|
|
$contentState = in_array((string) $run->outcome, [OperationRunOutcome::Blocked->value, OperationRunOutcome::Failed->value], true)
|
|
? 'missing_input'
|
|
: 'empty';
|
|
$actionability = in_array((string) $run->outcome, [OperationRunOutcome::Blocked->value, OperationRunOutcome::Failed->value], true)
|
|
? 'required'
|
|
: 'optional';
|
|
$primaryState = in_array((string) $run->outcome, [OperationRunOutcome::Blocked->value, OperationRunOutcome::Failed->value], true)
|
|
? 'created_but_not_usable'
|
|
: 'not_created';
|
|
|
|
return $this->makeEnvelope(
|
|
artifactFamily: 'artifact_run',
|
|
artifactKey: 'artifact_run:'.$run->getKey(),
|
|
workspaceId: (int) $run->workspace_id,
|
|
tenantId: $run->tenant_id !== null ? (int) $run->tenant_id : null,
|
|
executionOutcome: $run->outcome !== null ? (string) $run->outcome : null,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: 'unknown',
|
|
publicationReadiness: OperationCatalog::governanceArtifactFamily((string) $run->type) === 'review_pack'
|
|
|| OperationCatalog::governanceArtifactFamily((string) $run->type) === 'tenant_review'
|
|
? 'blocked'
|
|
: null,
|
|
supportState: 'normal',
|
|
actionability: $actionability,
|
|
primaryDomain: BadgeDomain::GovernanceArtifactExistence,
|
|
primaryState: $primaryState,
|
|
primaryExplanation: $reason?->shortExplanation ?? match ((string) $run->status) {
|
|
OperationRunStatus::Queued->value, OperationRunStatus::Running->value => 'The artifact-producing run is still in progress, so no artifact is available yet.',
|
|
default => 'The run finished without a usable artifact result.',
|
|
},
|
|
diagnosticLabel: BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, $run->outcome)->label,
|
|
reason: ArtifactTruthCause::fromReasonResolutionEnvelope($reason, ReasonPresenter::GOVERNANCE_ARTIFACT_TRUTH_ARTIFACT),
|
|
nextActionLabel: $this->nextActionLabel(
|
|
$actionability,
|
|
$reason,
|
|
$actionability === 'required'
|
|
? 'Inspect the blocked run details before retrying'
|
|
: 'Wait for the artifact-producing run to finish',
|
|
),
|
|
nextActionUrl: null,
|
|
relatedRunId: (int) $run->getKey(),
|
|
relatedArtifactUrl: null,
|
|
includePublicationDimension: OperationCatalog::governanceArtifactFamily((string) $run->type) === 'review_pack'
|
|
|| OperationCatalog::governanceArtifactFamily((string) $run->type) === 'tenant_review',
|
|
);
|
|
}
|
|
|
|
private function resolveArtifactForRun(OperationRun $run): BaselineSnapshot|EvidenceSnapshot|TenantReview|ReviewPack|null
|
|
{
|
|
return match (OperationCatalog::governanceArtifactFamily((string) $run->type)) {
|
|
'baseline_snapshot' => $run->relatedArtifactId() !== null
|
|
? BaselineSnapshot::query()->with('baselineProfile')->find($run->relatedArtifactId())
|
|
: null,
|
|
'evidence_snapshot' => EvidenceSnapshot::query()->with('tenant')->where('operation_run_id', (int) $run->getKey())->latest('id')->first(),
|
|
'tenant_review' => TenantReview::query()->with(['tenant', 'currentExportReviewPack'])->where('operation_run_id', (int) $run->getKey())->latest('id')->first(),
|
|
'review_pack' => ReviewPack::query()->with(['tenant', 'tenantReview'])->where('operation_run_id', (int) $run->getKey())->latest('id')->first(),
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
private function contentExplanation(string $contentState): string
|
|
{
|
|
return match ($contentState) {
|
|
'partial' => 'The artifact exists, but the captured content is incomplete for the primary operator task.',
|
|
'missing_input' => 'The artifact is blocked by missing upstream inputs or failed capture prerequisites.',
|
|
'metadata_only' => 'Only metadata was captured for this artifact. Use diagnostics for context, not as the primary truth signal.',
|
|
'reference_only' => 'Only reference-level placeholders were captured for this artifact.',
|
|
'empty' => 'The artifact row exists, but it does not contain usable captured content.',
|
|
'unsupported' => 'Structured support is limited for this artifact family, so the current rendering should be treated as diagnostic only.',
|
|
default => 'The artifact content is available for the intended workflow.',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, int> $reasons
|
|
*/
|
|
private function firstReasonCode(array $reasons): ?string
|
|
{
|
|
foreach ($reasons as $reason => $count) {
|
|
if ((int) $count > 0 && is_string($reason) && trim($reason) !== '') {
|
|
return trim($reason);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function nextActionLabel(
|
|
string $actionability,
|
|
?ReasonResolutionEnvelope $reason,
|
|
?string $fallback = null,
|
|
): ?string {
|
|
if ($actionability === 'none') {
|
|
return 'No action needed';
|
|
}
|
|
|
|
if (is_string($fallback) && trim($fallback) !== '') {
|
|
return $fallback;
|
|
}
|
|
|
|
if ($reason instanceof ReasonResolutionEnvelope && $reason->firstNextStep() !== null) {
|
|
return $reason->firstNextStep()?->label;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function makeEnvelope(
|
|
string $artifactFamily,
|
|
string $artifactKey,
|
|
int $workspaceId,
|
|
?int $tenantId,
|
|
?string $executionOutcome,
|
|
string $artifactExistence,
|
|
string $contentState,
|
|
string $freshnessState,
|
|
?string $publicationReadiness,
|
|
string $supportState,
|
|
string $actionability,
|
|
BadgeDomain $primaryDomain,
|
|
string $primaryState,
|
|
?string $primaryExplanation,
|
|
?string $diagnosticLabel,
|
|
?ArtifactTruthCause $reason,
|
|
?string $nextActionLabel,
|
|
?string $nextActionUrl,
|
|
?int $relatedRunId,
|
|
?string $relatedArtifactUrl,
|
|
bool $includePublicationDimension,
|
|
): ArtifactTruthEnvelope {
|
|
$primarySpec = BadgeCatalog::spec($primaryDomain, $primaryState);
|
|
$dimensions = [
|
|
$this->dimension(BadgeDomain::GovernanceArtifactExistence, $artifactExistence, 'artifact_existence', $primaryDomain === BadgeDomain::GovernanceArtifactExistence ? 'primary' : 'diagnostic'),
|
|
$this->dimension(BadgeDomain::GovernanceArtifactContent, $contentState, 'content_fidelity', $primaryDomain === BadgeDomain::GovernanceArtifactContent ? 'primary' : 'diagnostic'),
|
|
$this->dimension(BadgeDomain::GovernanceArtifactFreshness, $freshnessState, 'data_freshness', $primaryDomain === BadgeDomain::GovernanceArtifactFreshness ? 'primary' : 'diagnostic'),
|
|
$this->dimension(BadgeDomain::GovernanceArtifactActionability, $actionability, 'operator_actionability', 'diagnostic'),
|
|
];
|
|
|
|
if ($includePublicationDimension && $publicationReadiness !== null) {
|
|
$dimensions[] = $this->dimension(
|
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
|
$publicationReadiness,
|
|
'publication_readiness',
|
|
$primaryDomain === BadgeDomain::GovernanceArtifactPublicationReadiness ? 'primary' : 'diagnostic',
|
|
);
|
|
}
|
|
|
|
if ($executionOutcome !== null && trim($executionOutcome) !== '') {
|
|
$dimensions[] = $this->dimension(BadgeDomain::OperationRunOutcome, $executionOutcome, 'execution_outcome', 'diagnostic');
|
|
}
|
|
|
|
if ($supportState === 'limited_support') {
|
|
$dimensions[] = new ArtifactTruthDimension(
|
|
axis: 'support_maturity',
|
|
state: 'limited_support',
|
|
label: 'Support limited',
|
|
classification: 'diagnostic',
|
|
badgeDomain: BadgeDomain::GovernanceArtifactContent,
|
|
badgeState: 'unsupported',
|
|
);
|
|
}
|
|
|
|
return new ArtifactTruthEnvelope(
|
|
artifactFamily: $artifactFamily,
|
|
artifactKey: $artifactKey,
|
|
workspaceId: $workspaceId,
|
|
tenantId: $tenantId,
|
|
executionOutcome: $executionOutcome,
|
|
artifactExistence: $artifactExistence,
|
|
contentState: $contentState,
|
|
freshnessState: $freshnessState,
|
|
publicationReadiness: $publicationReadiness,
|
|
supportState: $supportState,
|
|
actionability: $actionability,
|
|
primaryLabel: $primarySpec->label,
|
|
primaryExplanation: $primaryExplanation,
|
|
diagnosticLabel: $diagnosticLabel,
|
|
nextActionLabel: $nextActionLabel,
|
|
nextActionUrl: $nextActionUrl,
|
|
relatedRunId: $relatedRunId,
|
|
relatedArtifactUrl: $relatedArtifactUrl,
|
|
dimensions: array_values($dimensions),
|
|
reason: $reason,
|
|
);
|
|
}
|
|
|
|
private function dimension(
|
|
BadgeDomain $domain,
|
|
string $state,
|
|
string $axis,
|
|
string $classification,
|
|
): ArtifactTruthDimension {
|
|
return new ArtifactTruthDimension(
|
|
axis: $axis,
|
|
state: $state,
|
|
label: BadgeCatalog::spec($domain, $state)->label,
|
|
classification: $classification,
|
|
badgeDomain: $domain,
|
|
badgeState: $state,
|
|
);
|
|
}
|
|
}
|