TenantAtlas/app/Services/Baselines/SnapshotRendering/BaselineSnapshotPresenter.php
ahmido 1f0cc5de56 feat: implement operator explanation layer (#191)
## Summary
- add the shared operator explanation layer with explanation families, trustworthiness semantics, count descriptors, and centralized badge mappings
- adopt explanation-first rendering across baseline compare, governance operation run detail, baseline snapshot presentation, tenant review detail, and review register rows
- extend reason translation, artifact-truth presentation, fallback ops UX messaging, and focused regression coverage for operator explanation semantics

## Testing
- vendor/bin/sail bin pint --dirty --format agent
- vendor/bin/sail artisan test --compact tests/Feature/Monitoring/OperationsTenantScopeTest.php tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php
- vendor/bin/sail artisan test --compact

## Notes
- Livewire v4 compatible
- panel provider registration remains in bootstrap/providers.php
- no destructive Filament actions were added or changed in this PR
- no new global-search behavior was introduced in this slice

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #191
2026-03-24 11:24:33 +00:00

425 lines
18 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Baselines\SnapshotRendering;
use App\Models\BaselineSnapshot;
use App\Models\BaselineSnapshotItem;
use App\Support\Badges\BadgeCatalog;
use App\Support\Badges\BadgeDomain;
use App\Support\Badges\BadgeRenderer;
use App\Support\Inventory\InventoryPolicyTypeMeta;
use App\Support\Ui\EnterpriseDetail\EnterpriseDetailBuilder;
use App\Support\Ui\EnterpriseDetail\EnterpriseDetailPageData;
use App\Support\Ui\EnterpriseDetail\EnterpriseDetailSectionFactory;
use App\Support\Ui\EnterpriseDetail\SummaryHeaderData;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Throwable;
final class BaselineSnapshotPresenter
{
public function __construct(
private readonly SnapshotTypeRendererRegistry $registry,
) {}
public function present(BaselineSnapshot $snapshot): RenderedSnapshot
{
$snapshot->loadMissing(['baselineProfile', 'items']);
$summary = is_array($snapshot->summary_jsonb) ? $snapshot->summary_jsonb : [];
$items = $snapshot->items instanceof EloquentCollection
? $snapshot->items->sortBy([
['policy_type', 'asc'],
['id', 'asc'],
])->values()
: collect();
$groups = $items
->groupBy(static fn (BaselineSnapshotItem $item): string => (string) $item->policy_type)
->map(fn (Collection $groupItems, string $policyType): RenderedSnapshotGroup => $this->presentGroup($policyType, $groupItems))
->sortBy(static fn (RenderedSnapshotGroup $group): string => mb_strtolower($group->label))
->values()
->all();
$summaryRows = array_map(
static fn (RenderedSnapshotGroup $group): array => [
'policyType' => $group->policyType,
'label' => $group->label,
'itemCount' => $group->itemCount,
'fidelity' => $group->fidelity->value,
'gapCount' => $group->gapSummary->count,
'capturedAt' => $group->capturedAt,
'coverageHint' => $group->coverageHint,
],
$groups,
);
$overallGapSummary = $this->summaryGapSummary($summary);
$overallGapCount = $overallGapSummary->count;
$overallFidelity = FidelityState::fromSummary($summary, $items->isNotEmpty());
return new RenderedSnapshot(
snapshotId: (int) $snapshot->getKey(),
baselineProfileName: $snapshot->baselineProfile?->name,
capturedAt: $snapshot->captured_at?->toIso8601String(),
snapshotIdentityHash: is_string($snapshot->snapshot_identity_hash) && trim($snapshot->snapshot_identity_hash) !== ''
? trim($snapshot->snapshot_identity_hash)
: null,
stateLabel: $this->gapStatusSpec($overallGapCount)->label,
fidelitySummary: $this->fidelitySummary($summary),
overallFidelity: $overallFidelity,
overallGapCount: $overallGapCount,
summaryRows: $summaryRows,
groups: $groups,
technicalDetail: [
'defaultCollapsed' => true,
'summaryPayload' => $summary,
'groupPayloads' => array_map(
static fn (RenderedSnapshotGroup $group): array => [
'label' => $group->label,
'renderingError' => $group->renderingError,
'payload' => $group->technicalPayload,
],
$groups,
),
],
hasItems: $items->isNotEmpty(),
);
}
/**
* @param list<array<string, mixed>> $relatedContext
*/
public function presentEnterpriseDetail(BaselineSnapshot $snapshot, array $relatedContext = []): EnterpriseDetailPageData
{
$rendered = $this->present($snapshot);
$factory = new EnterpriseDetailSectionFactory;
$truth = app(ArtifactTruthPresenter::class)->forBaselineSnapshot($snapshot);
$truthBadge = $factory->statusBadge(
$truth->primaryBadgeSpec()->label,
$truth->primaryBadgeSpec()->color,
$truth->primaryBadgeSpec()->icon,
$truth->primaryBadgeSpec()->iconColor,
);
$lifecycleSpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotLifecycle, $snapshot->lifecycleState()->value);
$lifecycleBadge = $factory->statusBadge(
$lifecycleSpec->label,
$lifecycleSpec->color,
$lifecycleSpec->icon,
$lifecycleSpec->iconColor,
);
$fidelitySpec = BadgeRenderer::spec(BadgeDomain::BaselineSnapshotFidelity, $rendered->overallFidelity->value);
$fidelityBadge = $factory->statusBadge(
$fidelitySpec->label,
$fidelitySpec->color,
$fidelitySpec->icon,
$fidelitySpec->iconColor,
);
$capturedItemCount = array_sum(array_map(
static fn (array $row): int => (int) ($row['itemCount'] ?? 0),
$rendered->summaryRows,
));
$currentTruth = $this->currentTruthPresentation($truth);
$currentTruthBadge = $factory->statusBadge(
$currentTruth['label'],
$currentTruth['color'],
$currentTruth['icon'],
$currentTruth['iconColor'],
);
$operatorExplanation = $truth->operatorExplanation;
return EnterpriseDetailBuilder::make('baseline_snapshot', 'workspace')
->header(new SummaryHeaderData(
title: $rendered->baselineProfileName ?? 'Baseline snapshot',
subtitle: 'Snapshot #'.$rendered->snapshotId,
statusBadges: [$truthBadge, $lifecycleBadge, $fidelityBadge],
keyFacts: [
$factory->keyFact('Captured', $this->formatTimestamp($rendered->capturedAt)),
$factory->keyFact('Current truth', $currentTruth['label'], badge: $currentTruthBadge),
$factory->keyFact('Evidence mix', $rendered->fidelitySummary),
$factory->keyFact('Captured items', $capturedItemCount),
],
descriptionHint: 'Current baseline truth, lifecycle proof, and coverage stay ahead of technical payload detail.',
))
->addSection(
$factory->viewSection(
id: 'artifact_truth',
kind: 'current_status',
title: 'Artifact truth',
view: 'filament.infolists.entries.governance-artifact-truth',
viewData: ['state' => $truth->toArray()],
description: 'Trustworthy artifact state stays separate from historical trace and support diagnostics.',
),
$factory->viewSection(
id: 'coverage_summary',
kind: 'current_status',
title: 'Coverage summary',
view: 'filament.infolists.entries.baseline-snapshot-summary-table',
viewData: ['rows' => $rendered->summaryRows],
emptyState: $factory->emptyState('No captured policy types are available in this snapshot.'),
),
$factory->viewSection(
id: 'related_context',
kind: 'related_context',
title: 'Related context',
view: 'filament.infolists.entries.related-context',
viewData: ['entries' => $relatedContext],
emptyState: $factory->emptyState('No related context is available for this record.'),
),
$factory->viewSection(
id: 'captured_policy_types',
kind: 'domain_detail',
title: 'Captured policy types',
view: 'filament.infolists.entries.baseline-snapshot-groups',
viewData: ['groups' => array_map(
static fn (RenderedSnapshotGroup $group): array => $group->toArray(),
$rendered->groups,
)],
emptyState: $factory->emptyState('No snapshot items were captured for this baseline snapshot.'),
),
)
->addSupportingCard(
$factory->supportingFactsCard(
kind: 'status',
title: 'Snapshot truth',
items: array_values(array_filter([
$factory->keyFact('Artifact truth', $truth->primaryLabel, badge: $truthBadge),
$operatorExplanation !== null
? $factory->keyFact('Result meaning', $operatorExplanation->evaluationResultLabel())
: null,
$operatorExplanation !== null
? $factory->keyFact('Result trust', $operatorExplanation->trustworthinessLabel())
: null,
$factory->keyFact('Lifecycle', $lifecycleSpec->label, badge: $lifecycleBadge),
$factory->keyFact('Current truth', $currentTruth['label'], badge: $currentTruthBadge),
$operatorExplanation !== null && $operatorExplanation->coverageStatement !== null
? $factory->keyFact('Coverage', $operatorExplanation->coverageStatement)
: null,
$factory->keyFact('Next step', $operatorExplanation?->nextActionText ?? $truth->nextStepText()),
])),
),
$factory->supportingFactsCard(
kind: 'coverage',
title: 'Coverage',
items: [
$factory->keyFact('Overall fidelity', $fidelitySpec->label, badge: $fidelityBadge),
$factory->keyFact('Evidence gaps', $rendered->overallGapCount),
$factory->keyFact('Captured items', $capturedItemCount),
],
),
$factory->supportingFactsCard(
kind: 'timestamps',
title: 'Capture timing',
items: [
$factory->keyFact('Captured', $this->formatTimestamp($rendered->capturedAt)),
$factory->keyFact('Completed', $this->formatTimestamp($snapshot->completed_at?->toIso8601String())),
$factory->keyFact('Failed', $this->formatTimestamp($snapshot->failed_at?->toIso8601String())),
$factory->keyFact('Identity hash', $rendered->snapshotIdentityHash),
],
),
)
->addTechnicalSection(
$factory->technicalDetail(
title: 'Technical detail',
entries: [
$factory->keyFact('Identity hash', $rendered->snapshotIdentityHash),
],
description: 'Technical payloads are secondary on purpose. Use them for debugging capture fidelity and renderer fallbacks.',
view: 'filament.infolists.entries.baseline-snapshot-technical-detail',
viewData: ['technical' => $rendered->technicalDetail],
),
)
->build();
}
/**
* @param Collection<int, BaselineSnapshotItem> $items
*/
private function presentGroup(string $policyType, Collection $items): RenderedSnapshotGroup
{
$renderer = $this->registry->rendererFor($policyType);
$fallbackRenderer = $this->registry->fallbackRenderer();
$renderingError = null;
$technicalPayload = $this->technicalPayload($items);
try {
$renderedItems = $items
->map(fn (BaselineSnapshotItem $item): RenderedSnapshotItem => $renderer->render($item))
->all();
} catch (Throwable) {
$renderedItems = $items
->map(fn (BaselineSnapshotItem $item): RenderedSnapshotItem => $fallbackRenderer->render($item))
->all();
$renderingError = 'Structured rendering failed for this policy type. Fallback metadata is shown instead.';
}
/** @var array<int, RenderedSnapshotItem> $renderedItems */
$groupFidelity = FidelityState::aggregate(array_map(
static fn (RenderedSnapshotItem $item): FidelityState => $item->fidelity,
$renderedItems,
));
$gapSummary = GapSummary::merge(array_map(
static fn (RenderedSnapshotItem $item): GapSummary => $item->gapSummary,
$renderedItems,
));
$capturedAt = collect($renderedItems)
->pluck('observedAt')
->filter(static fn (mixed $value): bool => is_string($value) && trim($value) !== '')
->sortDesc()
->first();
$coverageHint = $groupFidelity->coverageHint();
if ($coverageHint === null && $gapSummary->messages !== []) {
$coverageHint = $gapSummary->messages[0];
}
return new RenderedSnapshotGroup(
policyType: $policyType,
label: $this->typeLabel($policyType),
itemCount: $items->count(),
fidelity: $groupFidelity,
gapSummary: $gapSummary,
initiallyCollapsed: true,
items: $renderedItems,
renderingError: $renderingError,
coverageHint: $coverageHint,
capturedAt: is_string($capturedAt) ? $capturedAt : null,
technicalPayload: $technicalPayload,
);
}
/**
* @param Collection<int, BaselineSnapshotItem> $items
* @return array<string, mixed>
*/
private function technicalPayload(Collection $items): array
{
return [
'items' => $items
->map(static fn (BaselineSnapshotItem $item): array => [
'snapshot_item_id' => (int) $item->getKey(),
'policy_type' => (string) $item->policy_type,
'meta_jsonb' => is_array($item->meta_jsonb) ? $item->meta_jsonb : [],
])
->all(),
];
}
/**
* @param array<string, mixed> $summary
*/
private function summaryGapCount(array $summary): int
{
return $this->summaryGapSummary($summary)->count;
}
/**
* @param array<string, mixed> $summary
*/
private function fidelitySummary(array $summary): string
{
$counts = is_array($summary['fidelity_counts'] ?? null)
? $summary['fidelity_counts']
: [];
$content = is_numeric($counts['content'] ?? null) ? (int) $counts['content'] : 0;
$meta = is_numeric($counts['meta'] ?? null) ? (int) $counts['meta'] : 0;
return sprintf(
'%s %d, %s %d',
BadgeCatalog::spec(BadgeDomain::BaselineSnapshotFidelity, FidelityState::Full->value)->label,
$content,
BadgeCatalog::spec(BadgeDomain::BaselineSnapshotFidelity, FidelityState::ReferenceOnly->value)->label,
$meta,
);
}
/**
* @param array<string, mixed> $summary
*/
private function summaryGapSummary(array $summary): GapSummary
{
$gaps = is_array($summary['gaps'] ?? null) ? $summary['gaps'] : [];
$byReason = is_array($gaps['by_reason'] ?? null) ? $gaps['by_reason'] : [];
$gapSummary = GapSummary::fromReasonMap($byReason);
if ($byReason !== [] || ! is_numeric($gaps['count'] ?? null) || (int) $gaps['count'] <= 0) {
return $gapSummary;
}
return new GapSummary(
count: (int) $gaps['count'],
messages: ['Coverage gaps need review.'],
);
}
private function gapStatusSpec(int $gapCount): \App\Support\Badges\BadgeSpec
{
return BadgeRenderer::spec(
BadgeDomain::BaselineSnapshotGapStatus,
$gapCount > 0 ? 'gaps_present' : 'clear',
);
}
/**
* @return array{label: string, color: string, icon: string, iconColor: string}
*/
private function currentTruthPresentation(ArtifactTruthEnvelope $truth): array
{
return match ($truth->artifactExistence) {
'historical_only' => [
'label' => 'Historical trace',
'color' => 'gray',
'icon' => 'heroicon-m-clock',
'iconColor' => 'gray',
],
'created_but_not_usable' => [
'label' => 'Not compare input',
'color' => 'warning',
'icon' => 'heroicon-m-exclamation-triangle',
'iconColor' => 'warning',
],
default => [
'label' => 'Current baseline',
'color' => 'success',
'icon' => 'heroicon-m-check-badge',
'iconColor' => 'success',
],
};
}
private function typeLabel(string $policyType): string
{
return InventoryPolicyTypeMeta::baselineCompareLabel($policyType)
?? InventoryPolicyTypeMeta::label($policyType)
?? Str::headline($policyType);
}
private function formatTimestamp(?string $value): string
{
if ($value === null || trim($value) === '') {
return '—';
}
try {
return Carbon::parse($value)->toDayDateTimeString();
} catch (Throwable) {
return $value;
}
}
}