TenantAtlas/resources/views/filament/infolists/entries/evidence-gap-subjects.blade.php
ahmido c17255f854 feat: implement baseline subject resolution semantics (#193)
## Summary
- add the structured subject-resolution foundation for baseline compare and baseline capture, including capability guards, subject descriptors, resolution outcomes, and operator action categories
- persist structured evidence-gap subject records and update compare/capture surfaces, landing projections, and cleanup tooling to use the new contract
- add Spec 163 artifacts and focused Pest coverage for classification, determinism, cleanup, and DB-only rendering

## Validation
- `vendor/bin/sail bin pint --dirty --format agent`
- `vendor/bin/sail artisan test --compact tests/Unit/Support/Baselines tests/Feature/Baselines tests/Feature/Filament/OperationRunEnterpriseDetailPageTest.php`

## Notes
- verified locally that a fresh post-restart baseline compare run now writes structured `baseline_compare.evidence_gaps.subjects` records instead of the legacy broad payload shape
- excluded the separate `docs/product/spec-candidates.md` worktree change from this branch commit and PR

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #193
2026-03-25 12:40:45 +00:00

142 lines
8.7 KiB
PHP

@php
$summary = is_array($summary ?? null) ? $summary : [];
$buckets = is_array($buckets ?? null) ? $buckets : [];
$detailState = is_string($summary['detail_state'] ?? null) ? $summary['detail_state'] : 'no_gaps';
$structuralCount = is_numeric($summary['structural_count'] ?? null) ? (int) $summary['structural_count'] : 0;
$operationalCount = is_numeric($summary['operational_count'] ?? null) ? (int) $summary['operational_count'] : 0;
$transientCount = is_numeric($summary['transient_count'] ?? null) ? (int) $summary['transient_count'] : 0;
$tableContext = is_string($searchId ?? null) && $searchId !== '' ? $searchId : 'evidence-gap-search';
@endphp
@if ($detailState === 'legacy_broad_reason')
<div class="rounded-xl border border-danger-300 bg-danger-50/80 p-4 dark:border-danger-800 dark:bg-danger-950/30">
<div class="space-y-1">
<div class="text-sm font-semibold text-danger-950 dark:text-danger-100">
{{ __('baseline-compare.evidence_gap_legacy_title') }}
</div>
<p class="text-sm text-danger-900 dark:text-danger-200">
{{ __('baseline-compare.evidence_gap_legacy_body') }}
</p>
</div>
</div>
@elseif ($detailState === 'details_not_recorded' && $buckets === [])
<div class="rounded-xl border border-warning-300 bg-warning-50/80 p-4 dark:border-warning-800 dark:bg-warning-950/30">
<div class="space-y-1">
<div class="text-sm font-semibold text-warning-950 dark:text-warning-100">
{{ __('baseline-compare.evidence_gap_missing_details_title') }}
</div>
<p class="text-sm text-warning-900 dark:text-warning-200">
{{ __('baseline-compare.evidence_gap_missing_details_body') }}
</p>
</div>
</div>
@elseif ($buckets !== [])
<div class="space-y-4">
@if ($detailState === 'structured_details_recorded' && ($structuralCount > 0 || $operationalCount > 0 || $transientCount > 0))
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full bg-danger-100 px-2.5 py-1 text-xs font-medium text-danger-900 dark:bg-danger-900/30 dark:text-danger-100">
{{ __('baseline-compare.evidence_gap_structural', ['count' => $structuralCount]) }}
</span>
<span class="inline-flex items-center rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-900 dark:bg-primary-900/30 dark:text-primary-100">
{{ __('baseline-compare.evidence_gap_operational', ['count' => $operationalCount]) }}
</span>
<span class="inline-flex items-center rounded-full bg-warning-100 px-2.5 py-1 text-xs font-medium text-warning-900 dark:bg-warning-900/30 dark:text-warning-100">
{{ __('baseline-compare.evidence_gap_transient', ['count' => $transientCount]) }}
</span>
</div>
@endif
@if ($detailState === 'details_not_recorded')
<div class="rounded-xl border border-warning-300 bg-warning-50/80 p-4 dark:border-warning-800 dark:bg-warning-950/30">
<div class="space-y-1">
<div class="text-sm font-semibold text-warning-950 dark:text-warning-100">
{{ __('baseline-compare.evidence_gap_missing_details_title') }}
</div>
<p class="text-sm text-warning-900 dark:text-warning-200">
{{ __('baseline-compare.evidence_gap_missing_details_body') }}
</p>
</div>
</div>
@endif
<div class="grid gap-4 lg:grid-cols-2 xl:grid-cols-3">
@foreach ($buckets as $bucket)
@php
$reasonLabel = is_string($bucket['reason_label'] ?? null) ? $bucket['reason_label'] : 'Evidence gap';
$count = is_numeric($bucket['count'] ?? null) ? (int) $bucket['count'] : 0;
$recordedCount = is_numeric($bucket['recorded_count'] ?? null) ? (int) $bucket['recorded_count'] : 0;
$missingDetailCount = is_numeric($bucket['missing_detail_count'] ?? null) ? (int) $bucket['missing_detail_count'] : 0;
@endphp
<section class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
<div class="space-y-3">
<div class="space-y-1">
<h4 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ $reasonLabel }}
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ __('baseline-compare.evidence_gap_bucket_help') }}
</p>
</div>
<div class="flex flex-wrap gap-2">
<span class="inline-flex items-center rounded-full bg-warning-100 px-2.5 py-1 text-xs font-medium text-warning-900 dark:bg-warning-900/40 dark:text-warning-100">
{{ __('baseline-compare.evidence_gap_reason_affected', ['count' => $count]) }}
</span>
<span class="inline-flex items-center rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-900 dark:bg-primary-900/40 dark:text-primary-100">
{{ __('baseline-compare.evidence_gap_reason_recorded', ['count' => $recordedCount]) }}
</span>
@if ($missingDetailCount > 0)
<span class="inline-flex items-center rounded-full bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
{{ __('baseline-compare.evidence_gap_reason_missing_detail', ['count' => $missingDetailCount]) }}
</span>
@endif
@if ((int) ($bucket['structural_count'] ?? 0) > 0)
<span class="inline-flex items-center rounded-full bg-danger-100 px-2.5 py-1 text-xs font-medium text-danger-900 dark:bg-danger-900/30 dark:text-danger-100">
{{ __('baseline-compare.evidence_gap_bucket_structural', ['count' => (int) $bucket['structural_count']]) }}
</span>
@endif
@if ((int) ($bucket['operational_count'] ?? 0) > 0)
<span class="inline-flex items-center rounded-full bg-primary-100 px-2.5 py-1 text-xs font-medium text-primary-900 dark:bg-primary-900/30 dark:text-primary-100">
{{ __('baseline-compare.evidence_gap_bucket_operational', ['count' => (int) $bucket['operational_count']]) }}
</span>
@endif
@if ((int) ($bucket['transient_count'] ?? 0) > 0)
<span class="inline-flex items-center rounded-full bg-warning-100 px-2.5 py-1 text-xs font-medium text-warning-900 dark:bg-warning-900/30 dark:text-warning-100">
{{ __('baseline-compare.evidence_gap_bucket_transient', ['count' => (int) $bucket['transient_count']]) }}
</span>
@endif
</div>
@if ($missingDetailCount > 0)
<div class="rounded-lg border border-gray-200 bg-gray-50 px-3 py-3 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/60 dark:text-gray-200">
{{ __('baseline-compare.evidence_gap_missing_reason_body', ['count' => $missingDetailCount]) }}
</div>
@endif
</div>
</section>
@endforeach
</div>
<div class="space-y-2">
<div class="space-y-1">
<h4 class="text-sm font-semibold text-gray-950 dark:text-white">
{{ __('baseline-compare.evidence_gap_search_label') }}
</h4>
<p class="text-xs text-gray-500 dark:text-gray-400">
{{ __('baseline-compare.evidence_gap_search_help') }}
</p>
</div>
@livewire(
\App\Livewire\BaselineCompareEvidenceGapTable::class,
[
'buckets' => $buckets,
'context' => $tableContext,
],
key('baseline-compare-evidence-gap-table-'.$tableContext)
)
</div>
</div>
@endif