TenantAtlas/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
ahmido 8426741068 feat: add baseline snapshot truth guards (#189)
## Summary
- add explicit BaselineSnapshot lifecycle truth with conservative backfill and a shared truth resolver
- block baseline compare from building, incomplete, or superseded snapshots and align workspace/tenant UI truth surfaces with effective snapshot state
- surface artifact truth separately from operation outcome across baseline profile, snapshot, compare, and operation run pages

## Testing
- integrated browser smoke test on the active feature surfaces
- `vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineSnapshotTruthSurfaceTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`
- targeted baseline lifecycle and compare guard coverage added in Pest
- `vendor/bin/sail bin pint --dirty --format agent`

## Notes
- Livewire v4 compliance preserved
- no panel provider registration changes were needed; Laravel 12 providers remain in `bootstrap/providers.php`
- global search remains disabled for the affected baseline resources by design
- destructive actions remain confirmation-gated; capture and compare actions keep their existing authorization and confirmation behavior
- no new panel assets were added; existing deploy flow for `filament:assets` is unchanged

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #189
2026-03-23 11:32:00 +00:00

791 lines
38 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\BaselineSnapshotTruthResolver;
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,
private readonly BaselineSnapshotTruthResolver $snapshotTruthResolver,
) {}
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);
$effectiveSnapshot = $snapshot->baselineProfile !== null
? $this->snapshotTruthResolver->resolveEffectiveSnapshot($snapshot->baselineProfile)
: null;
$isHistorical = $this->snapshotTruthResolver->isHistoricallySuperseded($snapshot, $effectiveSnapshot);
$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->snapshotTruthResolver->artifactReasonCode($snapshot, $effectiveSnapshot)
?? $this->firstReasonCode($severeGapReasons);
$reason = $this->reasonPresenter->forArtifactTruth($reasonCode, 'artifact_truth');
$artifactExistence = match (true) {
$isHistorical => 'historical_only',
$snapshot->isBuilding(), $snapshot->isIncomplete() => 'created_but_not_usable',
! $snapshot->isConsumable() => 'created_but_not_usable',
default => 'created',
};
$contentState = match ($fidelity) {
FidelityState::Full => $snapshot->isIncomplete()
? ($hasItems ? 'partial' : 'missing_input')
: ($severeGapReasons === [] ? 'trusted' : 'partial'),
FidelityState::Partial => $snapshot->isBuilding() ? 'missing_input' : 'partial',
FidelityState::ReferenceOnly => $snapshot->isBuilding() ? 'missing_input' : 'reference_only',
FidelityState::Unsupported => $snapshot->isBuilding() ? 'missing_input' : ($hasItems ? 'unsupported' : 'trusted'),
};
if (($snapshot->isBuilding() || $snapshot->isIncomplete()) && $reasonCode !== null) {
$contentState = 'missing_input';
}
$freshnessState = match (true) {
$snapshot->isBuilding() => 'unknown',
$isHistorical => 'stale',
default => 'current',
};
$supportState = in_array($contentState, ['reference_only', 'unsupported'], true) ? 'limited_support' : 'normal';
$actionability = match (true) {
$artifactExistence === 'historical_only' => 'none',
$snapshot->isBuilding() => 'optional',
$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',
$reason?->shortExplanation ?? 'This snapshot remains readable for history, but a newer complete snapshot is the current baseline truth.',
BadgeCatalog::spec(BadgeDomain::BaselineSnapshotLifecycle, 'superseded')->label,
],
$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.',
BadgeCatalog::spec(BadgeDomain::BaselineSnapshotLifecycle, $snapshot->lifecycleState()->value)->label,
],
$contentState !== 'trusted' => [
BadgeDomain::GovernanceArtifactContent,
$contentState,
$reason?->shortExplanation ?? $this->contentExplanation($contentState),
$supportState === 'limited_support'
? 'Support limited'
: BadgeCatalog::spec(BadgeDomain::BaselineSnapshotLifecycle, $snapshot->lifecycleState()->value)->label,
],
default => [
BadgeDomain::GovernanceArtifactContent,
'trusted',
$hasItems
? 'Structured capture content is available for this baseline snapshot.'
: 'This empty baseline snapshot completed successfully and can still be used for compare.',
BadgeCatalog::spec(BadgeDomain::BaselineSnapshotLifecycle, $snapshot->lifecycleState()->value)->label,
],
};
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,
);
}
}