Compare commits
30 Commits
dev
...
259-compli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09ba297247 | ||
|
|
0517305381 | ||
| 966b7af472 | |||
|
|
1bf369b561 | ||
|
|
a2bb5b7729 | ||
|
|
bb78049271 | ||
| 7d17d39060 | |||
|
|
a35cd88bff | ||
| 926b0fe4f3 | |||
|
|
a74a6791ad | ||
| 52ebf63af1 | |||
|
|
2e2b125107 | ||
|
|
4b0dc2a62e | ||
|
|
34351a281d | ||
| 51ea80ca05 | |||
|
|
e36bd3ca9c | ||
| b511b08371 | |||
|
|
f53f149f99 | ||
| 2fa8fc0f87 | |||
|
|
44e6a1eb05 | ||
|
|
4f7c1a6c94 | ||
|
|
4325e1ed8d | ||
|
|
4ae4c2ee95 | ||
|
|
32b6dcb937 | ||
|
|
f7bc4f2787 | ||
|
|
0739018ee5 | ||
|
|
9a02261f5c | ||
|
|
65ec1d5904 | ||
|
|
f05857c276 | ||
|
|
9f5d3293c5 |
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -266,6 +266,8 @@ ## Active Technologies
|
|||||||
- PostgreSQL via existing `workspace_settings` rows plus existing audit log records; no new table or billing/account model (251-commercial-entitlements-billing-state)
|
- PostgreSQL via existing `workspace_settings` rows plus existing audit log records; no new table or billing/account model (251-commercial-entitlements-billing-state)
|
||||||
- PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities` (253-remove-findings-backfill-runtime-surfaces)
|
- PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities` (253-remove-findings-backfill-runtime-surfaces)
|
||||||
- PostgreSQL existing `findings`, `operation_runs`, `audit_logs`, and related runtime tables only; no new persistence, migration, or data backfill is planned (253-remove-findings-backfill-runtime-surfaces)
|
- PostgreSQL existing `findings`, `operation_runs`, `audit_logs`, and related runtime tables only; no new persistence, migration, or data backfill is planned (253-remove-findings-backfill-runtime-surfaces)
|
||||||
|
- PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing canonical-control, evidence, tenant-review, RBAC, localization, and audit seams (259-compliance-evidence-mapping)
|
||||||
|
- PostgreSQL via existing `tenant_reviews`, `tenant_review_sections`, `evidence_snapshots`, `evidence_snapshot_items`, `findings`, `finding_exceptions`, memberships, and `audit_logs`; no new persistence table planned (259-compliance-evidence-mapping)
|
||||||
|
|
||||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||||
|
|
||||||
@ -300,9 +302,9 @@ ## Code Style
|
|||||||
PHP 8.4.15: Follow standard conventions
|
PHP 8.4.15: Follow standard conventions
|
||||||
|
|
||||||
## Recent Changes
|
## Recent Changes
|
||||||
|
- 259-compliance-evidence-mapping: Added PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing canonical-control, evidence, tenant-review, RBAC, localization, and audit seams
|
||||||
- 253-remove-findings-backfill-runtime-surfaces: Added PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities`
|
- 253-remove-findings-backfill-runtime-surfaces: Added PHP 8.4 (Laravel 12) + Laravel 12 + Filament v5 + Livewire v4 + Pest; existing `UiEnforcement`, `OperationUxPresenter`, `OperationRunService`, `OperationCatalog`, `SystemOperationRunLinks`, `OperationRunLinks`, `AuditRecorder`, `WorkspaceAuditLogger`, and `PlatformCapabilities`
|
||||||
- 251-commercial-entitlements-billing-state: Added PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4, existing workspace settings stack (`SettingsRegistry`, `SettingsResolver`, `SettingsWriter`), `WorkspaceEntitlementResolver`, `ReviewPackService`, system directory detail page
|
- 251-commercial-entitlements-billing-state: Added PHP 8.4 (Laravel 12) + Filament v5 + Livewire v4, existing workspace settings stack (`SettingsRegistry`, `SettingsResolver`, `SettingsWriter`), `WorkspaceEntitlementResolver`, `ReviewPackService`, system directory detail page
|
||||||
- 249-customer-review-workspace: Added PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing review/evidence/review-pack/audit/RBAC support services
|
|
||||||
<!-- MANUAL ADDITIONS START -->
|
<!-- MANUAL ADDITIONS START -->
|
||||||
|
|
||||||
### Pre-production compatibility check
|
### Pre-production compatibility check
|
||||||
|
|||||||
@ -5,16 +5,21 @@
|
|||||||
namespace App\Filament\Pages\Reviews;
|
namespace App\Filament\Pages\Reviews;
|
||||||
|
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
|
use App\Models\EvidenceSnapshot;
|
||||||
|
use App\Models\FindingException;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\ReviewPack;
|
use App\Models\ReviewPack;
|
||||||
use App\Models\TenantReview;
|
use App\Models\TenantReview;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
|
use App\Services\Audit\WorkspaceAuditLogger;
|
||||||
use App\Services\ReviewPackService;
|
use App\Services\ReviewPackService;
|
||||||
use App\Services\TenantReviews\TenantReviewRegisterService;
|
use App\Services\TenantReviews\TenantReviewRegisterService;
|
||||||
|
use App\Support\Audit\AuditActionId;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Findings\FindingOutcomeSemantics;
|
use App\Support\Findings\FindingOutcomeSemantics;
|
||||||
use App\Support\Filament\TablePaginationProfiles;
|
use App\Support\Filament\TablePaginationProfiles;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
use App\Support\Navigation\CanonicalNavigationContext;
|
use App\Support\Navigation\CanonicalNavigationContext;
|
||||||
use App\Support\ReviewPackStatus;
|
use App\Support\ReviewPackStatus;
|
||||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||||
@ -36,6 +41,7 @@
|
|||||||
use Filament\Tables\Filters\SelectFilter;
|
use Filament\Tables\Filters\SelectFilter;
|
||||||
use Filament\Tables\Table;
|
use Filament\Tables\Table;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
use UnitEnum;
|
use UnitEnum;
|
||||||
|
|
||||||
@ -45,7 +51,7 @@ class CustomerReviewWorkspace extends Page implements HasTable
|
|||||||
|
|
||||||
public const string DETAIL_CONTEXT_QUERY_KEY = 'customer_workspace';
|
public const string DETAIL_CONTEXT_QUERY_KEY = 'customer_workspace';
|
||||||
|
|
||||||
private const string SOURCE_SURFACE = 'customer_review_workspace';
|
public const string SOURCE_SURFACE = 'customer_review_workspace';
|
||||||
|
|
||||||
protected static bool $isDiscovered = false;
|
protected static bool $isDiscovered = false;
|
||||||
|
|
||||||
@ -109,6 +115,7 @@ public function mount(): void
|
|||||||
$this->authorizePageAccess();
|
$this->authorizePageAccess();
|
||||||
$this->applyRequestedTenantPrefilter();
|
$this->applyRequestedTenantPrefilter();
|
||||||
$this->mountInteractsWithTable();
|
$this->mountInteractsWithTable();
|
||||||
|
$this->auditWorkspaceOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
@ -158,22 +165,26 @@ public function table(Table $table): Table
|
|||||||
->iconColor(fn (Tenant $record): ?string => $this->latestReviewStateIconColor($record))
|
->iconColor(fn (Tenant $record): ?string => $this->latestReviewStateIconColor($record))
|
||||||
->description(fn (Tenant $record): ?string => $this->reviewOutcomeDescription($record))
|
->description(fn (Tenant $record): ?string => $this->reviewOutcomeDescription($record))
|
||||||
->wrap(),
|
->wrap(),
|
||||||
TextColumn::make('finding_summary')
|
TextColumn::make('control_readiness')
|
||||||
->label(__('localization.review.key_findings'))
|
->label(__('localization.review.control_readiness'))
|
||||||
->getStateUsing(fn (Tenant $record): string => $this->findingSummary($record))
|
->badge()
|
||||||
|
->getStateUsing(fn (Tenant $record): string => $this->controlReadinessLabel($record))
|
||||||
|
->color(fn (Tenant $record): string => $this->controlReadinessColor($record))
|
||||||
|
->description(fn (Tenant $record): string => $this->controlReadinessDescription($record))
|
||||||
->wrap(),
|
->wrap(),
|
||||||
TextColumn::make('accepted_risk_summary')
|
TextColumn::make('evidence_basis')
|
||||||
->label(__('localization.review.accepted_risks'))
|
->label(__('localization.review.evidence_basis'))
|
||||||
->getStateUsing(fn (Tenant $record): string => $this->acceptedRiskSummary($record))
|
->getStateUsing(fn (Tenant $record): string => $this->controlEvidenceBasisSummary($record))
|
||||||
|
->wrap(),
|
||||||
|
TextColumn::make('recommended_next_action')
|
||||||
|
->label(__('localization.review.recommended_next_action'))
|
||||||
|
->getStateUsing(fn (Tenant $record): string => $this->controlRecommendedNextAction($record))
|
||||||
->wrap(),
|
->wrap(),
|
||||||
TextColumn::make('published_at')
|
TextColumn::make('published_at')
|
||||||
->label(__('localization.review.published'))
|
->label(__('localization.review.published'))
|
||||||
->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString())
|
->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString())
|
||||||
->dateTime()
|
->dateTime()
|
||||||
->placeholder('—'),
|
->placeholder('—'),
|
||||||
TextColumn::make('review_pack_state')
|
|
||||||
->label(__('localization.review.review_pack'))
|
|
||||||
->getStateUsing(fn (Tenant $record): string => $this->reviewPackAvailability($record)),
|
|
||||||
])
|
])
|
||||||
->filters([
|
->filters([
|
||||||
SelectFilter::make('tenant_id')
|
SelectFilter::make('tenant_id')
|
||||||
@ -195,18 +206,12 @@ public function table(Table $table): Table
|
|||||||
->icon('heroicon-o-arrow-top-right-on-square')
|
->icon('heroicon-o-arrow-top-right-on-square')
|
||||||
->url(fn (Tenant $record): ?string => $this->latestReviewUrl($record))
|
->url(fn (Tenant $record): ?string => $this->latestReviewUrl($record))
|
||||||
->visible(fn (Tenant $record): bool => $this->latestPublishedReview($record) instanceof TenantReview),
|
->visible(fn (Tenant $record): bool => $this->latestPublishedReview($record) instanceof TenantReview),
|
||||||
Action::make('download_review_pack')
|
|
||||||
->label(__('localization.review.download_review_pack'))
|
|
||||||
->icon('heroicon-o-arrow-down-tray')
|
|
||||||
->url(fn (Tenant $record): ?string => $this->latestReviewPackDownloadUrl($record))
|
|
||||||
->openUrlInNewTab()
|
|
||||||
->visible(fn (Tenant $record): bool => is_string($this->latestReviewPackDownloadUrl($record))),
|
|
||||||
])
|
])
|
||||||
->bulkActions([])
|
->bulkActions([])
|
||||||
->emptyStateHeading(__('localization.review.no_entitled_tenants'))
|
->emptyStateHeading(__('localization.review.no_released_customer_reviews'))
|
||||||
->emptyStateDescription(fn (): string => $this->hasActiveFilters()
|
->emptyStateDescription(fn (): string => $this->hasActiveFilters()
|
||||||
? __('localization.review.clear_filters_description')
|
? __('localization.review.clear_filters_description')
|
||||||
: __('localization.review.adjust_filters_description'))
|
: __('localization.review.no_released_customer_reviews_description'))
|
||||||
->emptyStateActions([
|
->emptyStateActions([
|
||||||
Action::make('clear_filters_empty')
|
Action::make('clear_filters_empty')
|
||||||
->label(__('localization.review.clear_filters'))
|
->label(__('localization.review.clear_filters'))
|
||||||
@ -260,6 +265,34 @@ private function authorizePageAccess(): void
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function auditWorkspaceOpen(): void
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$workspace = $this->workspace();
|
||||||
|
|
||||||
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(WorkspaceAuditLogger::class)->log(
|
||||||
|
workspace: $workspace,
|
||||||
|
action: AuditActionId::CustomerReviewWorkspaceOpened,
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'source_surface' => self::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => $this->currentTenantFilterId(),
|
||||||
|
'entitled_tenant_count' => count($this->authorizedTenants()),
|
||||||
|
'interpretation_version' => $this->currentTenantFilterInterpretationVersion(),
|
||||||
|
'interpretation_versions' => $this->visibleInterpretationVersions(),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actor: $user,
|
||||||
|
resourceType: 'customer_review_workspace',
|
||||||
|
resourceId: (string) $workspace->getKey(),
|
||||||
|
targetLabel: __('localization.review.customer_review_workspace'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private function workspaceQuery(): Builder
|
private function workspaceQuery(): Builder
|
||||||
{
|
{
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
@ -361,13 +394,20 @@ private function latestReviewUrl(Tenant $tenant): ?string
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->appendQuery(
|
$query = array_filter(
|
||||||
TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant'),
|
|
||||||
array_replace(
|
array_replace(
|
||||||
[self::DETAIL_CONTEXT_QUERY_KEY => 1],
|
[self::DETAIL_CONTEXT_QUERY_KEY => 1],
|
||||||
|
[
|
||||||
|
'source_surface' => self::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => $this->currentTenantFilterId(),
|
||||||
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||||
|
],
|
||||||
$this->navigationContext()?->toQuery() ?? [],
|
$this->navigationContext()?->toQuery() ?? [],
|
||||||
),
|
),
|
||||||
|
static fn (mixed $value): bool => $value !== null && $value !== '',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return $this->appendQuery(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant'), $query);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function latestReviewPack(Tenant $tenant): ?ReviewPack
|
private function latestReviewPack(Tenant $tenant): ?ReviewPack
|
||||||
@ -477,6 +517,148 @@ private function reviewOutcomeDescription(Tenant $tenant): ?string
|
|||||||
return trim($primaryReason.' '.__('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.');
|
return trim($primaryReason.' '.__('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function controlReadinessLabel(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$control = $this->primaryControlSummary($tenant);
|
||||||
|
|
||||||
|
if ($control === null) {
|
||||||
|
return __('localization.review.control_readiness_unmapped');
|
||||||
|
}
|
||||||
|
|
||||||
|
$label = $control['readiness_label'] ?? null;
|
||||||
|
|
||||||
|
return is_string($label) && trim($label) !== ''
|
||||||
|
? $label
|
||||||
|
: ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controlReadinessColor(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
return match ((string) ($this->primaryControlSummary($tenant)['readiness_bucket'] ?? 'unmapped')) {
|
||||||
|
'follow_up_required' => 'warning',
|
||||||
|
'review_recommended' => 'info',
|
||||||
|
'evidence_on_record' => 'success',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controlReadinessDescription(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$review = $this->latestPublishedReview($tenant);
|
||||||
|
|
||||||
|
if (! $review instanceof TenantReview) {
|
||||||
|
return __('localization.review.no_published_review_available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$controls = $review->controlInterpretationControls();
|
||||||
|
$version = $review->controlInterpretationVersion();
|
||||||
|
$displayLabel = $review->controlInterpretation()['display_label'] ?? null;
|
||||||
|
$prefixParts = array_values(array_filter([
|
||||||
|
is_string($displayLabel) && trim($displayLabel) !== '' ? $displayLabel : null,
|
||||||
|
$version !== null ? __('localization.review.interpretation_version_short', ['version' => $version]) : null,
|
||||||
|
]));
|
||||||
|
$prefix = $prefixParts === [] ? '' : implode(' · ', $prefixParts).' ';
|
||||||
|
|
||||||
|
if ($controls === []) {
|
||||||
|
return $prefix.__('localization.review.control_readiness_unmapped_description');
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = collect($controls)
|
||||||
|
->take(2)
|
||||||
|
->map(function (array $control): string {
|
||||||
|
$name = is_string($control['control_name'] ?? null) ? $control['control_name'] : __('localization.review.control');
|
||||||
|
$label = is_string($control['readiness_label'] ?? null)
|
||||||
|
? $control['readiness_label']
|
||||||
|
: ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended'));
|
||||||
|
|
||||||
|
return $name.': '.$label;
|
||||||
|
})
|
||||||
|
->implode(' · ');
|
||||||
|
|
||||||
|
$remaining = count($controls) - 2;
|
||||||
|
|
||||||
|
if ($remaining > 0) {
|
||||||
|
$summary .= ' · '.__('localization.review.additional_controls', ['count' => $remaining]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$limitations = $this->controlLimitationSummary($review);
|
||||||
|
|
||||||
|
return trim($prefix.$summary.($limitations !== null ? ' '.$limitations : ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controlEvidenceBasisSummary(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$control = $this->primaryControlSummary($tenant);
|
||||||
|
|
||||||
|
if ($control === null) {
|
||||||
|
return __('localization.review.control_evidence_unmapped');
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = $control['evidence_basis_summary'] ?? null;
|
||||||
|
|
||||||
|
return is_string($summary) && trim($summary) !== ''
|
||||||
|
? $summary
|
||||||
|
: __('localization.review.control_evidence_unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controlRecommendedNextAction(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$control = $this->primaryControlSummary($tenant);
|
||||||
|
|
||||||
|
if ($control === null) {
|
||||||
|
return __('localization.review.control_recommendation_unmapped');
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $control['recommended_next_action'] ?? null;
|
||||||
|
|
||||||
|
return is_string($action) && trim($action) !== ''
|
||||||
|
? $action
|
||||||
|
: __('localization.review.no_action_needed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function primaryControlSummary(Tenant $tenant): ?array
|
||||||
|
{
|
||||||
|
$review = $this->latestPublishedReview($tenant);
|
||||||
|
|
||||||
|
if (! $review instanceof TenantReview) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$controls = collect($review->controlInterpretationControls());
|
||||||
|
|
||||||
|
return $controls
|
||||||
|
->sortBy(static fn (array $control): int => match ((string) ($control['readiness_bucket'] ?? '')) {
|
||||||
|
'follow_up_required' => 0,
|
||||||
|
'review_recommended' => 1,
|
||||||
|
'evidence_on_record' => 2,
|
||||||
|
default => 3,
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function controlLimitationSummary(TenantReview $review): ?string
|
||||||
|
{
|
||||||
|
$counts = $review->controlInterpretationLimitationCounts();
|
||||||
|
|
||||||
|
if ($counts === []) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$labels = collect($counts)
|
||||||
|
->filter(static fn (int $count): bool => $count > 0)
|
||||||
|
->keys()
|
||||||
|
->map(static fn (string $flag): string => ComplianceEvidenceMappingV1::limitationLabel($flag))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return $labels === []
|
||||||
|
? null
|
||||||
|
: __('localization.review.control_limitations_summary', ['limitations' => implode(', ', $labels)]);
|
||||||
|
}
|
||||||
|
|
||||||
private function findingSummary(Tenant $tenant): string
|
private function findingSummary(Tenant $tenant): string
|
||||||
{
|
{
|
||||||
$review = $this->latestPublishedReview($tenant);
|
$review = $this->latestPublishedReview($tenant);
|
||||||
@ -518,31 +700,160 @@ private function acceptedRiskSummary(Tenant $tenant): string
|
|||||||
$validGovernedCount = (int) ($riskAcceptance['valid_governed_count'] ?? 0);
|
$validGovernedCount = (int) ($riskAcceptance['valid_governed_count'] ?? 0);
|
||||||
$warningCount = (int) ($riskAcceptance['warning_count'] ?? 0);
|
$warningCount = (int) ($riskAcceptance['warning_count'] ?? 0);
|
||||||
|
|
||||||
return match (true) {
|
$countSummary = match (true) {
|
||||||
$statusMarkedCount === 0 => __('localization.review.no_accepted_risks_recorded'),
|
$statusMarkedCount === 0 => __('localization.review.no_accepted_risks_recorded'),
|
||||||
$warningCount > 0 => __('localization.review.accepted_risks_need_follow_up', ['warnings' => $warningCount, 'total' => $statusMarkedCount]),
|
$warningCount > 0 => __('localization.review.accepted_risks_need_follow_up', ['warnings' => $warningCount, 'total' => $statusMarkedCount]),
|
||||||
$validGovernedCount > 0 => __('localization.review.accepted_risks_governed', ['count' => $validGovernedCount]),
|
$validGovernedCount > 0 => __('localization.review.accepted_risks_governed', ['count' => $validGovernedCount]),
|
||||||
default => __('localization.review.accepted_risks_on_record', ['count' => $statusMarkedCount]),
|
default => __('localization.review.accepted_risks_on_record', ['count' => $statusMarkedCount]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$accountability = $this->acceptedRiskAccountability($tenant);
|
||||||
|
|
||||||
|
return $accountability === null
|
||||||
|
? $countSummary
|
||||||
|
: $countSummary.' '.$accountability;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function reviewPackAvailability(Tenant $tenant): string
|
private function reviewPackAvailability(Tenant $tenant): string
|
||||||
{
|
{
|
||||||
|
if (! $this->latestPublishedReview($tenant) instanceof TenantReview) {
|
||||||
|
return __('localization.review.no_published_review_available');
|
||||||
|
}
|
||||||
|
|
||||||
$pack = $this->latestReviewPack($tenant);
|
$pack = $this->latestReviewPack($tenant);
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
if (! $pack instanceof ReviewPack) {
|
if (! $pack instanceof ReviewPack) {
|
||||||
return __('localization.review.unavailable');
|
return __('localization.review.no_current_review_pack');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user instanceof User || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||||
|
return __('localization.review.review_pack_access_unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||||
return __('localization.review.unavailable');
|
return __('localization.review.review_pack_unavailable');
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||||
return __('localization.review.unavailable');
|
return __('localization.review.review_pack_expired');
|
||||||
}
|
}
|
||||||
|
|
||||||
return __('localization.review.available');
|
return __('localization.review.review_pack_available');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evidenceProofAvailability(Tenant $tenant): string
|
||||||
|
{
|
||||||
|
$review = $this->latestPublishedReview($tenant);
|
||||||
|
|
||||||
|
if (! $review instanceof TenantReview) {
|
||||||
|
return __('localization.review.no_published_review_available');
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshot = $review->evidenceSnapshot;
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
||||||
|
return __('localization.review.evidence_proof_absent');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
||||||
|
return __('localization.review.evidence_proof_access_unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
||||||
|
return __('localization.review.evidence_proof_expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('localization.review.evidence_proof_available');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function visibleInterpretationVersions(): array
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$workspace = $this->workspace();
|
||||||
|
|
||||||
|
if (! $user instanceof User || ! $workspace instanceof Workspace) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return app(TenantReviewRegisterService::class)
|
||||||
|
->latestPublishedQuery($user, $workspace)
|
||||||
|
->get()
|
||||||
|
->map(static fn (TenantReview $review): ?string => $review->controlInterpretationVersion())
|
||||||
|
->filter()
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentTenantFilterInterpretationVersion(): ?string
|
||||||
|
{
|
||||||
|
$tenantId = $this->currentTenantFilterId();
|
||||||
|
|
||||||
|
if ($tenantId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = $this->authorizedTenants()[$tenantId] ?? null;
|
||||||
|
|
||||||
|
if (! $tenant instanceof Tenant) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tenant->tenantReviews()->published()
|
||||||
|
->latest('published_at')
|
||||||
|
->latest('generated_at')
|
||||||
|
->latest('id')
|
||||||
|
->first()
|
||||||
|
?->controlInterpretationVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function acceptedRiskAccountability(Tenant $tenant): ?string
|
||||||
|
{
|
||||||
|
$exception = FindingException::query()
|
||||||
|
->with(['owner', 'approver', 'currentDecision'])
|
||||||
|
->where('workspace_id', (int) $tenant->workspace_id)
|
||||||
|
->where('tenant_id', (int) $tenant->getKey())
|
||||||
|
->current()
|
||||||
|
->orderByRaw("case when current_validity_state in ('valid', 'expiring') then 0 else 1 end")
|
||||||
|
->latest('approved_at')
|
||||||
|
->latest('requested_at')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $exception instanceof FindingException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$accountable = $exception->owner?->name
|
||||||
|
?? $exception->approver?->name;
|
||||||
|
$decisionType = $exception->currentDecision?->decision_type;
|
||||||
|
$reviewDue = $exception->review_due_at ?? $exception->expires_at;
|
||||||
|
$reason = is_string($exception->request_reason) ? trim($exception->request_reason) : '';
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
if (is_string($accountable) && trim($accountable) !== '') {
|
||||||
|
$parts[] = $reviewDue === null
|
||||||
|
? __('localization.review.accepted_risk_accountable', ['name' => $accountable])
|
||||||
|
: __('localization.review.accepted_risk_accountable_until', [
|
||||||
|
'name' => $accountable,
|
||||||
|
'date' => $reviewDue->toDateString(),
|
||||||
|
]);
|
||||||
|
} elseif (is_string($decisionType) && trim($decisionType) !== '') {
|
||||||
|
$parts[] = __('localization.review.accepted_risk_partial_accountability');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($reason !== '') {
|
||||||
|
$parts[] = __('localization.review.accepted_risk_reason', [
|
||||||
|
'reason' => Str::limit($reason, 160),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parts === [] ? null : implode(' ', $parts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function navigationContext(): ?CanonicalNavigationContext
|
private function navigationContext(): ?CanonicalNavigationContext
|
||||||
|
|||||||
@ -174,9 +174,12 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->label('Operation')
|
->label('Operation')
|
||||||
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
||||||
->url(fn (EvidenceSnapshot $record): ?string => $record->operation_run_id ? OperationRunLinks::tenantlessView((int) $record->operation_run_id) : null)
|
->url(fn (EvidenceSnapshot $record): ?string => $record->operation_run_id ? OperationRunLinks::tenantlessView((int) $record->operation_run_id) : null)
|
||||||
->openUrlInNewTab(),
|
->openUrlInNewTab()
|
||||||
TextEntry::make('fingerprint')->copyable()->placeholder('—')->columnSpanFull()->fontFamily('mono'),
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
TextEntry::make('previous_fingerprint')->copyable()->placeholder('—')->columnSpanFull()->fontFamily('mono'),
|
TextEntry::make('fingerprint')->copyable()->placeholder('—')->columnSpanFull()->fontFamily('mono')
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
|
TextEntry::make('previous_fingerprint')->copyable()->placeholder('—')->columnSpanFull()->fontFamily('mono')
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
])
|
])
|
||||||
->columns(2),
|
->columns(2),
|
||||||
Section::make('Summary')
|
Section::make('Summary')
|
||||||
@ -222,6 +225,7 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->label('Raw summary JSON')
|
->label('Raw summary JSON')
|
||||||
->view('filament.infolists.entries.snapshot-json')
|
->view('filament.infolists.entries.snapshot-json')
|
||||||
->state(fn (EvidenceSnapshotItem $record): array => is_array($record->summary_payload) ? $record->summary_payload : [])
|
->state(fn (EvidenceSnapshotItem $record): array => is_array($record->summary_payload) ? $record->summary_payload : [])
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow())
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
])
|
])
|
||||||
->columns(4),
|
->columns(4),
|
||||||
@ -236,7 +240,7 @@ public static function relatedContextEntries(EvidenceSnapshot $record): array
|
|||||||
{
|
{
|
||||||
$entries = [];
|
$entries = [];
|
||||||
|
|
||||||
if (is_numeric($record->operation_run_id)) {
|
if (! static::isCustomerWorkspaceFlow() && is_numeric($record->operation_run_id)) {
|
||||||
$entries[] = RelatedContextEntry::available(
|
$entries[] = RelatedContextEntry::available(
|
||||||
key: 'operation_run',
|
key: 'operation_run',
|
||||||
label: 'Operation',
|
label: 'Operation',
|
||||||
@ -255,12 +259,18 @@ public static function relatedContextEntries(EvidenceSnapshot $record): array
|
|||||||
->first();
|
->first();
|
||||||
|
|
||||||
if ($pack instanceof \App\Models\ReviewPack && $pack->tenant instanceof Tenant) {
|
if ($pack instanceof \App\Models\ReviewPack && $pack->tenant instanceof Tenant) {
|
||||||
|
$packUrl = ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $pack->tenant);
|
||||||
|
|
||||||
|
if (static::isCustomerWorkspaceFlow()) {
|
||||||
|
$packUrl = static::appendQuery($packUrl, static::customerWorkspaceContextQuery());
|
||||||
|
}
|
||||||
|
|
||||||
$entries[] = RelatedContextEntry::available(
|
$entries[] = RelatedContextEntry::available(
|
||||||
key: 'review_pack',
|
key: 'review_pack',
|
||||||
label: 'Review pack',
|
label: 'Review pack',
|
||||||
value: sprintf('#%d', (int) $pack->getKey()),
|
value: sprintf('#%d', (int) $pack->getKey()),
|
||||||
secondaryValue: 'Inspect the latest executive-pack output for this evidence basis.',
|
secondaryValue: 'Inspect the latest executive-pack output for this evidence basis.',
|
||||||
targetUrl: ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $pack->tenant),
|
targetUrl: $packUrl,
|
||||||
targetKind: 'direct_record',
|
targetKind: 'direct_record',
|
||||||
priority: 20,
|
priority: 20,
|
||||||
actionLabel: 'View review pack',
|
actionLabel: 'View review pack',
|
||||||
@ -285,6 +295,36 @@ public static function relatedContextEntries(EvidenceSnapshot $record): array
|
|||||||
return $entries;
|
return $entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isCustomerWorkspaceFlow(): bool
|
||||||
|
{
|
||||||
|
return request()->query('source_surface') === CustomerReviewWorkspace::SOURCE_SURFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function customerWorkspaceContextQuery(): array
|
||||||
|
{
|
||||||
|
return array_filter([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => request()->query('review_id'),
|
||||||
|
'interpretation_version' => request()->query('interpretation_version'),
|
||||||
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||||
|
], static fn (mixed $value): bool => $value !== null && $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $query
|
||||||
|
*/
|
||||||
|
private static function appendQuery(string $url, array $query): string
|
||||||
|
{
|
||||||
|
if ($query === []) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
|
||||||
|
}
|
||||||
|
|
||||||
public static function table(Table $table): Table
|
public static function table(Table $table): Table
|
||||||
{
|
{
|
||||||
return $table
|
return $table
|
||||||
|
|||||||
@ -5,8 +5,13 @@
|
|||||||
namespace App\Filament\Resources\EvidenceSnapshotResource\Pages;
|
namespace App\Filament\Resources\EvidenceSnapshotResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
|
use App\Models\EvidenceSnapshot;
|
||||||
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\Audit\WorkspaceAuditLogger;
|
||||||
use App\Services\Evidence\EvidenceSnapshotService;
|
use App\Services\Evidence\EvidenceSnapshotService;
|
||||||
|
use App\Support\Audit\AuditActionId;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Rbac\UiEnforcement;
|
use App\Support\Rbac\UiEnforcement;
|
||||||
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
||||||
@ -20,6 +25,13 @@ class ViewEvidenceSnapshot extends ViewRecord
|
|||||||
{
|
{
|
||||||
protected static string $resource = EvidenceSnapshotResource::class;
|
protected static string $resource = EvidenceSnapshotResource::class;
|
||||||
|
|
||||||
|
public function mount(int|string $record): void
|
||||||
|
{
|
||||||
|
parent::mount($record);
|
||||||
|
|
||||||
|
$this->auditCustomerWorkspaceProofOpen();
|
||||||
|
}
|
||||||
|
|
||||||
protected function resolveRecord(int|string $key): Model
|
protected function resolveRecord(int|string $key): Model
|
||||||
{
|
{
|
||||||
return EvidenceSnapshotResource::resolveScopedRecordOrFail($key);
|
return EvidenceSnapshotResource::resolveScopedRecordOrFail($key);
|
||||||
@ -27,6 +39,10 @@ protected function resolveRecord(int|string $key): Model
|
|||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
|
if (EvidenceSnapshotResource::isCustomerWorkspaceFlow()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
$refreshRule = GovernanceActionCatalog::rule('refresh_evidence');
|
$refreshRule = GovernanceActionCatalog::rule('refresh_evidence');
|
||||||
$expireRule = GovernanceActionCatalog::rule('expire_snapshot');
|
$expireRule = GovernanceActionCatalog::rule('expire_snapshot');
|
||||||
|
|
||||||
@ -90,4 +106,44 @@ protected function getHeaderActions(): array
|
|||||||
->apply(),
|
->apply(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function auditCustomerWorkspaceProofOpen(): void
|
||||||
|
{
|
||||||
|
if (! EvidenceSnapshotResource::isCustomerWorkspaceFlow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$record = $this->record;
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $record instanceof EvidenceSnapshot || ! $user instanceof User) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tenant = $record->tenant;
|
||||||
|
|
||||||
|
if (! $tenant instanceof Tenant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
app(WorkspaceAuditLogger::class)->log(
|
||||||
|
workspace: $tenant->workspace,
|
||||||
|
action: AuditActionId::EvidenceSnapshotOpened,
|
||||||
|
context: [
|
||||||
|
'metadata' => [
|
||||||
|
'evidence_snapshot_id' => (int) $record->getKey(),
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => request()->query('review_id'),
|
||||||
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||||
|
'interpretation_version' => request()->query('interpretation_version'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
actor: $user,
|
||||||
|
resourceType: 'evidence_snapshot',
|
||||||
|
resourceId: (string) $record->getKey(),
|
||||||
|
targetLabel: sprintf('Evidence snapshot #%d', (int) $record->getKey()),
|
||||||
|
tenant: $tenant,
|
||||||
|
operationRunId: $record->operation_run_id !== null ? (int) $record->operation_run_id : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -148,7 +148,8 @@ public static function infolist(Schema $schema): Schema
|
|||||||
TextEntry::make('file_size')
|
TextEntry::make('file_size')
|
||||||
->label('File size')
|
->label('File size')
|
||||||
->formatStateUsing(fn ($state): string => $state ? Number::fileSize((int) $state) : '—'),
|
->formatStateUsing(fn ($state): string => $state ? Number::fileSize((int) $state) : '—'),
|
||||||
TextEntry::make('sha256')->label('SHA-256')->copyable()->placeholder('—'),
|
TextEntry::make('sha256')->label('SHA-256')->copyable()->placeholder('—')
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
])
|
])
|
||||||
->columns(2)
|
->columns(2)
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
@ -184,6 +185,7 @@ public static function infolist(Schema $schema): Schema
|
|||||||
->formatStateUsing(fn ($state): string => $state ? 'Yes' : 'No'),
|
->formatStateUsing(fn ($state): string => $state ? 'Yes' : 'No'),
|
||||||
])
|
])
|
||||||
->columns(2)
|
->columns(2)
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow())
|
||||||
->columnSpanFull(),
|
->columnSpanFull(),
|
||||||
|
|
||||||
Section::make('Metadata')
|
Section::make('Metadata')
|
||||||
@ -227,9 +229,12 @@ public static function infolist(Schema $schema): Schema
|
|||||||
return OperationRunLinks::tenantlessView((int) $record->operation_run_id);
|
return OperationRunLinks::tenantlessView((int) $record->operation_run_id);
|
||||||
})
|
})
|
||||||
->openUrlInNewTab()
|
->openUrlInNewTab()
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow())
|
||||||
->placeholder('—'),
|
->placeholder('—'),
|
||||||
TextEntry::make('fingerprint')->label('Fingerprint')->copyable()->placeholder('—'),
|
TextEntry::make('fingerprint')->label('Fingerprint')->copyable()->placeholder('—')
|
||||||
TextEntry::make('previous_fingerprint')->label('Previous fingerprint')->copyable()->placeholder('—'),
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
|
TextEntry::make('previous_fingerprint')->label('Previous fingerprint')->copyable()->placeholder('—')
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceFlow()),
|
||||||
TextEntry::make('created_at')->label('Created')->dateTime(),
|
TextEntry::make('created_at')->label('Created')->dateTime(),
|
||||||
])
|
])
|
||||||
->columns(2)
|
->columns(2)
|
||||||
@ -243,9 +248,7 @@ public static function infolist(Schema $schema): Schema
|
|||||||
TextEntry::make('evidenceSnapshot.id')
|
TextEntry::make('evidenceSnapshot.id')
|
||||||
->label('Snapshot')
|
->label('Snapshot')
|
||||||
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
->formatStateUsing(fn (?int $state): string => $state ? '#'.$state : '—')
|
||||||
->url(fn (ReviewPack $record): ?string => $record->evidenceSnapshot
|
->url(fn (ReviewPack $record): ?string => static::evidenceSnapshotUrl($record)),
|
||||||
? TenantEvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant)
|
|
||||||
: null),
|
|
||||||
TextEntry::make('evidenceSnapshot.completeness_state')
|
TextEntry::make('evidenceSnapshot.completeness_state')
|
||||||
->label('Snapshot completeness')
|
->label('Snapshot completeness')
|
||||||
->badge()
|
->badge()
|
||||||
@ -429,6 +432,36 @@ public static function getPages(): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function isCustomerWorkspaceFlow(): bool
|
||||||
|
{
|
||||||
|
return request()->query('source_surface') === CustomerReviewWorkspace::SOURCE_SURFACE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function evidenceSnapshotUrl(ReviewPack $record): ?string
|
||||||
|
{
|
||||||
|
if (! $record->evidenceSnapshot) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = TenantEvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant);
|
||||||
|
|
||||||
|
return static::isCustomerWorkspaceFlow()
|
||||||
|
? static::appendQuery($url, ['source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE])
|
||||||
|
: $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $query
|
||||||
|
*/
|
||||||
|
private static function appendQuery(string $url, array $query): string
|
||||||
|
{
|
||||||
|
if ($query === []) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
|
||||||
|
}
|
||||||
|
|
||||||
private static function truthEnvelope(ReviewPack $record, bool $fresh = false): ArtifactTruthEnvelope
|
private static function truthEnvelope(ReviewPack $record, bool $fresh = false): ArtifactTruthEnvelope
|
||||||
{
|
{
|
||||||
$presenter = app(ArtifactTruthPresenter::class);
|
$presenter = app(ArtifactTruthPresenter::class);
|
||||||
|
|||||||
@ -19,6 +19,20 @@ class ViewReviewPack extends ViewRecord
|
|||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
|
if (ReviewPackResource::isCustomerWorkspaceFlow()) {
|
||||||
|
return [
|
||||||
|
Actions\Action::make('download')
|
||||||
|
->label('Download')
|
||||||
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
|
->color('primary')
|
||||||
|
->visible(fn (): bool => $this->record->status === ReviewPackStatus::Ready->value)
|
||||||
|
->url(fn (): string => app(ReviewPackService::class)->generateDownloadUrl($this->record, [
|
||||||
|
'source_surface' => \App\Filament\Pages\Reviews\CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
]))
|
||||||
|
->openUrlInNewTab(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$regenerateAction = UiEnforcement::forAction(
|
$regenerateAction = UiEnforcement::forAction(
|
||||||
Actions\Action::make('regenerate')
|
Actions\Action::make('regenerate')
|
||||||
->label('Regenerate')
|
->label('Regenerate')
|
||||||
|
|||||||
@ -215,6 +215,7 @@ public static function infolist(Schema $schema): Schema
|
|||||||
TextEntry::make('fingerprint')
|
TextEntry::make('fingerprint')
|
||||||
->copyable()
|
->copyable()
|
||||||
->placeholder('—')
|
->placeholder('—')
|
||||||
|
->hidden(fn (): bool => static::isCustomerWorkspaceMode())
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->fontFamily('mono')
|
->fontFamily('mono')
|
||||||
->size(TextSize::ExtraSmall),
|
->size(TextSize::ExtraSmall),
|
||||||
@ -639,6 +640,9 @@ private static function summaryPresentation(TenantReview $record): array
|
|||||||
$highlights = is_array($summary['highlights'] ?? null) ? $summary['highlights'] : [];
|
$highlights = is_array($summary['highlights'] ?? null) ? $summary['highlights'] : [];
|
||||||
$findingOutcomeSummary = static::findingOutcomeSummary($summary);
|
$findingOutcomeSummary = static::findingOutcomeSummary($summary);
|
||||||
$findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : [];
|
$findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : [];
|
||||||
|
$controlInterpretation = is_array($summary['control_interpretation'] ?? null)
|
||||||
|
? $summary['control_interpretation']
|
||||||
|
: [];
|
||||||
|
|
||||||
if ($findingOutcomeSummary !== null) {
|
if ($findingOutcomeSummary !== null) {
|
||||||
$highlights[] = __('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.';
|
$highlights[] = __('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.';
|
||||||
@ -647,12 +651,20 @@ private static function summaryPresentation(TenantReview $record): array
|
|||||||
return [
|
return [
|
||||||
'operator_explanation' => $truthEnvelope->operatorExplanation?->toArray(),
|
'operator_explanation' => $truthEnvelope->operatorExplanation?->toArray(),
|
||||||
'compressed_outcome' => static::compressedOutcome($record)->toArray(),
|
'compressed_outcome' => static::compressedOutcome($record)->toArray(),
|
||||||
'reason_semantics' => $reasonPresenter->semantics($truthEnvelope->reason?->toReasonResolutionEnvelope()),
|
'customer_workspace_mode' => static::isCustomerWorkspaceMode(),
|
||||||
|
'reason_semantics' => static::isCustomerWorkspaceMode()
|
||||||
|
? []
|
||||||
|
: $reasonPresenter->semantics($truthEnvelope->reason?->toReasonResolutionEnvelope()),
|
||||||
'highlights' => $highlights,
|
'highlights' => $highlights,
|
||||||
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
||||||
'publish_blockers' => is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
'publish_blockers' => is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
||||||
'context_links' => static::summaryContextLinks($record),
|
'context_links' => static::summaryContextLinks($record, static::isCustomerWorkspaceMode()),
|
||||||
'metrics' => [
|
'control_interpretation' => $controlInterpretation,
|
||||||
|
'metrics' => static::isCustomerWorkspaceMode() ? [
|
||||||
|
['label' => __('localization.review.findings'), 'value' => (string) ($summary['finding_count'] ?? 0)],
|
||||||
|
['label' => __('localization.review.pending_verification'), 'value' => (string) ($findingOutcomes[FindingOutcomeSemantics::OUTCOME_RESOLVED_PENDING_VERIFICATION] ?? 0)],
|
||||||
|
['label' => __('localization.review.verified_cleared'), 'value' => (string) ($findingOutcomes[FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED] ?? 0)],
|
||||||
|
] : [
|
||||||
['label' => __('localization.review.findings'), 'value' => (string) ($summary['finding_count'] ?? 0)],
|
['label' => __('localization.review.findings'), 'value' => (string) ($summary['finding_count'] ?? 0)],
|
||||||
['label' => __('localization.review.reports'), 'value' => (string) ($summary['report_count'] ?? 0)],
|
['label' => __('localization.review.reports'), 'value' => (string) ($summary['report_count'] ?? 0)],
|
||||||
['label' => __('localization.review.operations'), 'value' => (string) ($summary['operation_count'] ?? 0)],
|
['label' => __('localization.review.operations'), 'value' => (string) ($summary['operation_count'] ?? 0)],
|
||||||
@ -664,13 +676,13 @@ private static function summaryPresentation(TenantReview $record): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<int, array{title:string,label:string,url:string,description:string}>
|
* @return array<int, array{title:string,label:string,url:?string,description:string}>
|
||||||
*/
|
*/
|
||||||
private static function summaryContextLinks(TenantReview $record): array
|
private static function summaryContextLinks(TenantReview $record, bool $customerWorkspaceMode = false): array
|
||||||
{
|
{
|
||||||
$links = [];
|
$links = [];
|
||||||
|
|
||||||
if (is_numeric($record->operation_run_id)) {
|
if (! $customerWorkspaceMode && is_numeric($record->operation_run_id)) {
|
||||||
$links[] = [
|
$links[] = [
|
||||||
'title' => __('localization.review.operation'),
|
'title' => __('localization.review.operation'),
|
||||||
'label' => __('localization.review.open_operation'),
|
'label' => __('localization.review.open_operation'),
|
||||||
@ -679,7 +691,7 @@ private static function summaryContextLinks(TenantReview $record): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($record->currentExportReviewPack && $record->tenant) {
|
if (! $customerWorkspaceMode && $record->currentExportReviewPack && $record->tenant) {
|
||||||
$links[] = [
|
$links[] = [
|
||||||
'title' => __('localization.review.executive_pack'),
|
'title' => __('localization.review.executive_pack'),
|
||||||
'label' => __('localization.review.view_executive_pack'),
|
'label' => __('localization.review.view_executive_pack'),
|
||||||
@ -698,11 +710,23 @@ private static function summaryContextLinks(TenantReview $record): array
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($record->evidenceSnapshot && $record->tenant) {
|
if ($record->evidenceSnapshot && $record->tenant) {
|
||||||
|
$user = auth()->user();
|
||||||
|
$canViewEvidence = $user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $record->tenant);
|
||||||
|
$evidenceUrl = $canViewEvidence
|
||||||
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($customerWorkspaceMode && $evidenceUrl !== null) {
|
||||||
|
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($record));
|
||||||
|
}
|
||||||
|
|
||||||
$links[] = [
|
$links[] = [
|
||||||
'title' => __('localization.review.evidence_snapshot'),
|
'title' => __('localization.review.evidence_snapshot'),
|
||||||
'label' => __('localization.review.view_evidence_snapshot'),
|
'label' => __('localization.review.view_evidence_snapshot'),
|
||||||
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $record->evidenceSnapshot], tenant: $record->tenant),
|
'url' => $evidenceUrl,
|
||||||
'description' => __('localization.review.evidence_snapshot_description'),
|
'description' => $canViewEvidence
|
||||||
|
? __('localization.review.evidence_snapshot_description')
|
||||||
|
: __('localization.review.evidence_proof_access_unavailable'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,6 +742,24 @@ private static function sectionPresentation(TenantReviewSection $section): array
|
|||||||
$render = is_array($section->render_payload) ? $section->render_payload : [];
|
$render = is_array($section->render_payload) ? $section->render_payload : [];
|
||||||
$review = $section->tenantReview;
|
$review = $section->tenantReview;
|
||||||
$tenant = $section->tenant;
|
$tenant = $section->tenant;
|
||||||
|
$links = [];
|
||||||
|
|
||||||
|
if ($section->isControlInterpretation() && $review instanceof TenantReview && $tenant instanceof Tenant && $review->evidenceSnapshot instanceof EvidenceSnapshot) {
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($user instanceof User && $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
||||||
|
$evidenceUrl = EvidenceSnapshotResource::getUrl('view', ['record' => $review->evidenceSnapshot], tenant: $tenant);
|
||||||
|
|
||||||
|
if (static::isCustomerWorkspaceMode()) {
|
||||||
|
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($review));
|
||||||
|
}
|
||||||
|
|
||||||
|
$links[] = [
|
||||||
|
'label' => __('localization.review.view_evidence_snapshot'),
|
||||||
|
'url' => $evidenceUrl,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'summary' => collect($summary)->map(function (mixed $value, string $key): ?array {
|
'summary' => collect($summary)->map(function (mixed $value, string $key): ?array {
|
||||||
@ -735,7 +777,8 @@ private static function sectionPresentation(TenantReviewSection $section): array
|
|||||||
'disclosure' => is_string($render['disclosure'] ?? null) ? $render['disclosure'] : null,
|
'disclosure' => is_string($render['disclosure'] ?? null) ? $render['disclosure'] : null,
|
||||||
'next_actions' => is_array($render['next_actions'] ?? null) ? $render['next_actions'] : [],
|
'next_actions' => is_array($render['next_actions'] ?? null) ? $render['next_actions'] : [],
|
||||||
'empty_state' => is_string($render['empty_state'] ?? null) ? $render['empty_state'] : null,
|
'empty_state' => is_string($render['empty_state'] ?? null) ? $render['empty_state'] : null,
|
||||||
'links' => [],
|
'is_control_interpretation' => $section->isControlInterpretation(),
|
||||||
|
'links' => $links,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -783,4 +826,34 @@ private static function findingOutcomeSummary(array $summary): ?string
|
|||||||
|
|
||||||
return app(FindingOutcomeSemantics::class)->compactOutcomeSummary($outcomeCounts);
|
return app(FindingOutcomeSemantics::class)->compactOutcomeSummary($outcomeCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static function isCustomerWorkspaceMode(): bool
|
||||||
|
{
|
||||||
|
return request()->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private static function customerWorkspaceEvidenceQuery(TenantReview $record): array
|
||||||
|
{
|
||||||
|
return array_filter([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => (int) $record->getKey(),
|
||||||
|
'interpretation_version' => $record->controlInterpretationVersion(),
|
||||||
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||||
|
], static fn (mixed $value): bool => $value !== null && $value !== '');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $query
|
||||||
|
*/
|
||||||
|
private static function appendQuery(string $url, array $query): string
|
||||||
|
{
|
||||||
|
if ($query === []) {
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url.(str_contains($url, '?') ? '&' : '?').http_build_query($query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,15 +6,18 @@
|
|||||||
|
|
||||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
|
use App\Models\ReviewPack;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\TenantReview;
|
use App\Models\TenantReview;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\ReviewPackService;
|
||||||
use App\Services\Audit\WorkspaceAuditLogger;
|
use App\Services\Audit\WorkspaceAuditLogger;
|
||||||
use App\Services\TenantReviews\TenantReviewLifecycleService;
|
use App\Services\TenantReviews\TenantReviewLifecycleService;
|
||||||
use App\Services\TenantReviews\TenantReviewService;
|
use App\Services\TenantReviews\TenantReviewService;
|
||||||
use App\Support\Audit\AuditActionId;
|
use App\Support\Audit\AuditActionId;
|
||||||
use App\Support\Auth\Capabilities;
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Rbac\UiEnforcement;
|
use App\Support\Rbac\UiEnforcement;
|
||||||
|
use App\Support\ReviewPackStatus;
|
||||||
use App\Support\TenantReviewStatus;
|
use App\Support\TenantReviewStatus;
|
||||||
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
@ -64,6 +67,12 @@ protected function authorizeAccess(): void
|
|||||||
|
|
||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
|
if ($this->isCustomerWorkspaceView()) {
|
||||||
|
return [
|
||||||
|
$this->downloadCurrentReviewPackAction(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
$secondaryActions = $this->secondaryLifecycleActions();
|
$secondaryActions = $this->secondaryLifecycleActions();
|
||||||
|
|
||||||
return array_values(array_filter([
|
return array_values(array_filter([
|
||||||
@ -343,6 +352,77 @@ private function archiveReviewAction(): Actions\Action
|
|||||||
->apply();
|
->apply();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function downloadCurrentReviewPackAction(): Actions\Action
|
||||||
|
{
|
||||||
|
return Actions\Action::make('download_current_review_pack')
|
||||||
|
->label(__('localization.review.download_current_review_pack'))
|
||||||
|
->icon('heroicon-o-arrow-down-tray')
|
||||||
|
->color('primary')
|
||||||
|
->disabled(fn (): bool => $this->currentReviewPackDownloadUrl() === null)
|
||||||
|
->tooltip(fn (): ?string => $this->currentReviewPackUnavailableReason())
|
||||||
|
->url(fn (): ?string => $this->currentReviewPackDownloadUrl())
|
||||||
|
->openUrlInNewTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentReviewPackDownloadUrl(): ?string
|
||||||
|
{
|
||||||
|
$pack = $this->record->currentExportReviewPack;
|
||||||
|
$tenant = $this->record->tenant;
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $pack instanceof ReviewPack || ! $tenant instanceof Tenant || ! $user instanceof User) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => (int) $this->record->getKey(),
|
||||||
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||||
|
'interpretation_version' => $this->record->controlInterpretationVersion(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function currentReviewPackUnavailableReason(): ?string
|
||||||
|
{
|
||||||
|
if ($this->currentReviewPackDownloadUrl() !== null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pack = $this->record->currentExportReviewPack;
|
||||||
|
$tenant = $this->record->tenant;
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if (! $pack instanceof ReviewPack) {
|
||||||
|
return __('localization.review.customer_review_pack_missing');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user instanceof User || ! $tenant instanceof Tenant || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||||
|
return __('localization.review.customer_review_pack_forbidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||||
|
return __('localization.review.customer_review_pack_not_ready');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||||
|
return __('localization.review.customer_review_pack_expired');
|
||||||
|
}
|
||||||
|
|
||||||
|
return __('localization.review.customer_review_pack_unavailable');
|
||||||
|
}
|
||||||
|
|
||||||
private function isCustomerWorkspaceView(): bool
|
private function isCustomerWorkspaceView(): bool
|
||||||
{
|
{
|
||||||
return request()->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY);
|
return request()->boolean(CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY);
|
||||||
@ -367,7 +447,9 @@ private function auditCustomerWorkspaceOpen(): void
|
|||||||
context: [
|
context: [
|
||||||
'metadata' => [
|
'metadata' => [
|
||||||
'review_id' => (int) $this->record->getKey(),
|
'review_id' => (int) $this->record->getKey(),
|
||||||
'source_surface' => 'customer_review_workspace',
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => request()->query('tenant_filter_id'),
|
||||||
|
'interpretation_version' => $this->record->controlInterpretationVersion(),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
actor: $user,
|
actor: $user,
|
||||||
|
|||||||
@ -59,6 +59,9 @@ public function __invoke(Request $request, ReviewPack $reviewPack): StreamedResp
|
|||||||
? (int) $reviewPack->tenant_review_id
|
? (int) $reviewPack->tenant_review_id
|
||||||
: null,
|
: null,
|
||||||
'source_surface' => (string) $request->query('source_surface', 'review_pack'),
|
'source_surface' => (string) $request->query('source_surface', 'review_pack'),
|
||||||
|
'review_id' => $request->query('review_id'),
|
||||||
|
'tenant_filter_id' => $request->query('tenant_filter_id'),
|
||||||
|
'interpretation_version' => $request->query('interpretation_version'),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
actor: $user,
|
actor: $user,
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
|
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
use App\Support\TenantReviewCompletenessState;
|
use App\Support\TenantReviewCompletenessState;
|
||||||
use App\Support\TenantReviewStatus;
|
use App\Support\TenantReviewStatus;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
@ -205,4 +206,63 @@ public function canonicalControlReferences(): array
|
|||||||
? array_values(array_filter($references, static fn (mixed $reference): bool => is_array($reference)))
|
? array_values(array_filter($references, static fn (mixed $reference): bool => is_array($reference)))
|
||||||
: [];
|
: [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function controlInterpretation(): array
|
||||||
|
{
|
||||||
|
$summary = is_array($this->summary) ? $this->summary : [];
|
||||||
|
$interpretation = $summary['control_interpretation'] ?? [];
|
||||||
|
|
||||||
|
return is_array($interpretation) ? $interpretation : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function controlInterpretationVersion(): ?string
|
||||||
|
{
|
||||||
|
$version = $this->controlInterpretation()['version_key'] ?? null;
|
||||||
|
|
||||||
|
return is_string($version) && trim($version) !== '' ? $version : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function controlInterpretationControls(): array
|
||||||
|
{
|
||||||
|
$controls = $this->controlInterpretation()['controls'] ?? [];
|
||||||
|
|
||||||
|
return is_array($controls)
|
||||||
|
? array_values(array_filter($controls, static fn (mixed $control): bool => is_array($control)))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
public function controlInterpretationLimitationCounts(): array
|
||||||
|
{
|
||||||
|
$counts = $this->controlInterpretation()['limitation_counts'] ?? [];
|
||||||
|
|
||||||
|
if (! is_array($counts)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect($counts)
|
||||||
|
->mapWithKeys(static fn (mixed $count, string|int $key): array => [(string) $key => (int) $count])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function controlInterpretationSection(): ?TenantReviewSection
|
||||||
|
{
|
||||||
|
if ($this->relationLoaded('sections')) {
|
||||||
|
$section = $this->sections->firstWhere('section_key', ComplianceEvidenceMappingV1::SECTION_KEY);
|
||||||
|
|
||||||
|
return $section instanceof TenantReviewSection ? $section : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->sections()
|
||||||
|
->where('section_key', ComplianceEvidenceMappingV1::SECTION_KEY)
|
||||||
|
->first();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use App\Support\TenantReviewCompletenessState;
|
use App\Support\TenantReviewCompletenessState;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
@ -67,4 +68,26 @@ public function completenessEnum(): TenantReviewCompletenessState
|
|||||||
return TenantReviewCompletenessState::tryFrom((string) $this->completeness_state)
|
return TenantReviewCompletenessState::tryFrom((string) $this->completeness_state)
|
||||||
?? TenantReviewCompletenessState::Missing;
|
?? TenantReviewCompletenessState::Missing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isControlInterpretation(): bool
|
||||||
|
{
|
||||||
|
return (string) $this->section_key === ComplianceEvidenceMappingV1::SECTION_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public function controlInterpretationEntries(): array
|
||||||
|
{
|
||||||
|
if (! $this->isControlInterpretation()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderPayload = is_array($this->render_payload) ? $this->render_payload : [];
|
||||||
|
$entries = $renderPayload['entries'] ?? [];
|
||||||
|
|
||||||
|
return is_array($entries)
|
||||||
|
? array_values(array_filter($entries, static fn (mixed $entry): bool => is_array($entry)))
|
||||||
|
: [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,10 @@ public function compose(EvidenceSnapshot $snapshot, ?TenantReview $review = null
|
|||||||
$sectionStateCounts = $this->readinessGate->sectionStateCounts($sections);
|
$sectionStateCounts = $this->readinessGate->sectionStateCounts($sections);
|
||||||
$completeness = $this->readinessGate->completenessForSections($sections);
|
$completeness = $this->readinessGate->completenessForSections($sections);
|
||||||
$status = $this->readinessGate->statusForSections($sections);
|
$status = $this->readinessGate->statusForSections($sections);
|
||||||
|
$controlInterpretationSection = collect($sections)
|
||||||
|
->firstWhere('section_key', 'control_interpretation');
|
||||||
|
$operationsSection = collect($sections)
|
||||||
|
->firstWhere('section_key', 'operations_health');
|
||||||
|
|
||||||
if ($review instanceof TenantReview && $review->isPublished()) {
|
if ($review instanceof TenantReview && $review->isPublished()) {
|
||||||
$status = TenantReviewStatus::Published;
|
$status = TenantReviewStatus::Published;
|
||||||
@ -68,8 +72,18 @@ public function compose(EvidenceSnapshot $snapshot, ?TenantReview $review = null
|
|||||||
'canonical_controls' => is_array(data_get($sections, '0.summary_payload.canonical_controls'))
|
'canonical_controls' => is_array(data_get($sections, '0.summary_payload.canonical_controls'))
|
||||||
? data_get($sections, '0.summary_payload.canonical_controls')
|
? data_get($sections, '0.summary_payload.canonical_controls')
|
||||||
: [],
|
: [],
|
||||||
|
'control_interpretation' => is_array(data_get($controlInterpretationSection, 'summary_payload'))
|
||||||
|
? array_merge(
|
||||||
|
data_get($controlInterpretationSection, 'summary_payload'),
|
||||||
|
[
|
||||||
|
'controls' => is_array(data_get($controlInterpretationSection, 'render_payload.entries'))
|
||||||
|
? data_get($controlInterpretationSection, 'render_payload.entries')
|
||||||
|
: [],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: [],
|
||||||
'report_count' => 2,
|
'report_count' => 2,
|
||||||
'operation_count' => (int) data_get($sections, '5.summary_payload.operation_count', 0),
|
'operation_count' => (int) data_get($operationsSection, 'summary_payload.operation_count', 0),
|
||||||
'highlights' => data_get($sections, '0.render_payload.highlights', []),
|
'highlights' => data_get($sections, '0.render_payload.highlights', []),
|
||||||
'recommended_next_actions' => data_get($sections, '0.render_payload.next_actions', []),
|
'recommended_next_actions' => data_get($sections, '0.render_payload.next_actions', []),
|
||||||
'last_composed_at' => now()->toIso8601String(),
|
'last_composed_at' => now()->toIso8601String(),
|
||||||
|
|||||||
@ -81,6 +81,7 @@ public function customerWorkspaceTenantQuery(User $user, Workspace $workspace):
|
|||||||
return Tenant::query()
|
return Tenant::query()
|
||||||
->where('workspace_id', (int) $workspace->getKey())
|
->where('workspace_id', (int) $workspace->getKey())
|
||||||
->whereIn('id', $tenantIds === [] ? [-1] : $tenantIds)
|
->whereIn('id', $tenantIds === [] ? [-1] : $tenantIds)
|
||||||
|
->whereHas('tenantReviews', fn ($query) => $query->published())
|
||||||
->with([
|
->with([
|
||||||
'tenantReviews' => fn ($query) => $query
|
'tenantReviews' => fn ($query) => $query
|
||||||
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack'])
|
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack'])
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
use App\Models\EvidenceSnapshot;
|
use App\Models\EvidenceSnapshot;
|
||||||
use App\Models\EvidenceSnapshotItem;
|
use App\Models\EvidenceSnapshotItem;
|
||||||
use App\Support\Findings\FindingOutcomeSemantics;
|
use App\Support\Findings\FindingOutcomeSemantics;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
use App\Support\TenantReviewCompletenessState;
|
use App\Support\TenantReviewCompletenessState;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@ -15,6 +16,7 @@ final class TenantReviewSectionFactory
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly FindingOutcomeSemantics $findingOutcomeSemantics,
|
private readonly FindingOutcomeSemantics $findingOutcomeSemantics,
|
||||||
|
private readonly ComplianceEvidenceMappingV1 $complianceEvidenceMapping,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -29,8 +31,11 @@ public function make(EvidenceSnapshot $snapshot): array
|
|||||||
$baselineItem = $this->item($items, 'baseline_drift_posture');
|
$baselineItem = $this->item($items, 'baseline_drift_posture');
|
||||||
$operationsItem = $this->item($items, 'operations_summary');
|
$operationsItem = $this->item($items, 'operations_summary');
|
||||||
|
|
||||||
|
$controlInterpretation = $this->complianceEvidenceMapping->interpret($snapshot, $findingsItem);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$this->executiveSummarySection($snapshot, $findingsItem, $permissionItem, $rolesItem, $baselineItem, $operationsItem),
|
$this->executiveSummarySection($snapshot, $findingsItem, $permissionItem, $rolesItem, $baselineItem, $operationsItem),
|
||||||
|
$controlInterpretation['section'],
|
||||||
$this->openRisksSection($findingsItem),
|
$this->openRisksSection($findingsItem),
|
||||||
$this->acceptedRisksSection($findingsItem),
|
$this->acceptedRisksSection($findingsItem),
|
||||||
$this->permissionPostureSection($permissionItem, $rolesItem),
|
$this->permissionPostureSection($permissionItem, $rolesItem),
|
||||||
|
|||||||
@ -91,6 +91,7 @@ enum AuditActionId: string
|
|||||||
case EvidenceSnapshotCreated = 'evidence_snapshot.created';
|
case EvidenceSnapshotCreated = 'evidence_snapshot.created';
|
||||||
case EvidenceSnapshotRefreshed = 'evidence_snapshot.refreshed';
|
case EvidenceSnapshotRefreshed = 'evidence_snapshot.refreshed';
|
||||||
case EvidenceSnapshotExpired = 'evidence_snapshot.expired';
|
case EvidenceSnapshotExpired = 'evidence_snapshot.expired';
|
||||||
|
case EvidenceSnapshotOpened = 'evidence_snapshot.opened';
|
||||||
case TenantReviewCreated = 'tenant_review.created';
|
case TenantReviewCreated = 'tenant_review.created';
|
||||||
case TenantReviewRefreshed = 'tenant_review.refreshed';
|
case TenantReviewRefreshed = 'tenant_review.refreshed';
|
||||||
case TenantReviewPublished = 'tenant_review.published';
|
case TenantReviewPublished = 'tenant_review.published';
|
||||||
@ -98,6 +99,7 @@ enum AuditActionId: string
|
|||||||
case TenantReviewOpened = 'tenant_review.opened';
|
case TenantReviewOpened = 'tenant_review.opened';
|
||||||
case TenantReviewExported = 'tenant_review.exported';
|
case TenantReviewExported = 'tenant_review.exported';
|
||||||
case TenantReviewSuccessorCreated = 'tenant_review.successor_created';
|
case TenantReviewSuccessorCreated = 'tenant_review.successor_created';
|
||||||
|
case CustomerReviewWorkspaceOpened = 'customer_review_workspace.opened';
|
||||||
case ReviewPackDownloaded = 'review_pack.downloaded';
|
case ReviewPackDownloaded = 'review_pack.downloaded';
|
||||||
case TenantTriageReviewMarkedReviewed = 'tenant_triage_review.marked_reviewed';
|
case TenantTriageReviewMarkedReviewed = 'tenant_triage_review.marked_reviewed';
|
||||||
case TenantTriageReviewMarkedFollowUpNeeded = 'tenant_triage_review.marked_follow_up_needed';
|
case TenantTriageReviewMarkedFollowUpNeeded = 'tenant_triage_review.marked_follow_up_needed';
|
||||||
@ -241,6 +243,7 @@ private static function labels(): array
|
|||||||
self::EvidenceSnapshotCreated->value => 'Evidence snapshot created',
|
self::EvidenceSnapshotCreated->value => 'Evidence snapshot created',
|
||||||
self::EvidenceSnapshotRefreshed->value => 'Evidence snapshot refreshed',
|
self::EvidenceSnapshotRefreshed->value => 'Evidence snapshot refreshed',
|
||||||
self::EvidenceSnapshotExpired->value => 'Evidence snapshot expired',
|
self::EvidenceSnapshotExpired->value => 'Evidence snapshot expired',
|
||||||
|
self::EvidenceSnapshotOpened->value => 'Evidence snapshot opened',
|
||||||
self::TenantReviewCreated->value => 'Tenant review created',
|
self::TenantReviewCreated->value => 'Tenant review created',
|
||||||
self::TenantReviewRefreshed->value => 'Tenant review refreshed',
|
self::TenantReviewRefreshed->value => 'Tenant review refreshed',
|
||||||
self::TenantReviewPublished->value => 'Tenant review published',
|
self::TenantReviewPublished->value => 'Tenant review published',
|
||||||
@ -248,6 +251,7 @@ private static function labels(): array
|
|||||||
self::TenantReviewOpened->value => 'Tenant review opened',
|
self::TenantReviewOpened->value => 'Tenant review opened',
|
||||||
self::TenantReviewExported->value => 'Tenant review exported',
|
self::TenantReviewExported->value => 'Tenant review exported',
|
||||||
self::TenantReviewSuccessorCreated->value => 'Tenant review next cycle created',
|
self::TenantReviewSuccessorCreated->value => 'Tenant review next cycle created',
|
||||||
|
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||||
self::TenantTriageReviewMarkedReviewed->value => 'Triage review marked reviewed',
|
self::TenantTriageReviewMarkedReviewed->value => 'Triage review marked reviewed',
|
||||||
self::TenantTriageReviewMarkedFollowUpNeeded->value => 'Triage review marked follow-up needed',
|
self::TenantTriageReviewMarkedFollowUpNeeded->value => 'Triage review marked follow-up needed',
|
||||||
@ -337,6 +341,7 @@ private static function summaries(): array
|
|||||||
self::EvidenceSnapshotCreated->value => 'Evidence snapshot created',
|
self::EvidenceSnapshotCreated->value => 'Evidence snapshot created',
|
||||||
self::EvidenceSnapshotRefreshed->value => 'Evidence snapshot refreshed',
|
self::EvidenceSnapshotRefreshed->value => 'Evidence snapshot refreshed',
|
||||||
self::EvidenceSnapshotExpired->value => 'Evidence snapshot expired',
|
self::EvidenceSnapshotExpired->value => 'Evidence snapshot expired',
|
||||||
|
self::EvidenceSnapshotOpened->value => 'Evidence snapshot opened',
|
||||||
self::TenantReviewCreated->value => 'Tenant review created',
|
self::TenantReviewCreated->value => 'Tenant review created',
|
||||||
self::TenantReviewRefreshed->value => 'Tenant review refreshed',
|
self::TenantReviewRefreshed->value => 'Tenant review refreshed',
|
||||||
self::TenantReviewPublished->value => 'Tenant review published',
|
self::TenantReviewPublished->value => 'Tenant review published',
|
||||||
@ -344,6 +349,7 @@ private static function summaries(): array
|
|||||||
self::TenantReviewOpened->value => 'Tenant review opened',
|
self::TenantReviewOpened->value => 'Tenant review opened',
|
||||||
self::TenantReviewExported->value => 'Tenant review exported',
|
self::TenantReviewExported->value => 'Tenant review exported',
|
||||||
self::TenantReviewSuccessorCreated->value => 'Tenant review next cycle created',
|
self::TenantReviewSuccessorCreated->value => 'Tenant review next cycle created',
|
||||||
|
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||||
self::SupportDiagnosticsOpened->value => 'Support diagnostics opened',
|
self::SupportDiagnosticsOpened->value => 'Support diagnostics opened',
|
||||||
self::SupportRequestCreated->value => 'Support request created',
|
self::SupportRequestCreated->value => 'Support request created',
|
||||||
|
|||||||
@ -0,0 +1,515 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Governance\Controls;
|
||||||
|
|
||||||
|
use App\Models\EvidenceSnapshot;
|
||||||
|
use App\Models\EvidenceSnapshotItem;
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Support\TenantReviewCompletenessState;
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
final readonly class ComplianceEvidenceMappingV1
|
||||||
|
{
|
||||||
|
public const string VERSION_KEY = 'compliance_evidence_mapping.v1';
|
||||||
|
|
||||||
|
public const string SECTION_KEY = 'control_interpretation';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private CanonicalControlCatalog $catalog,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* summary: array<string, mixed>,
|
||||||
|
* section: array<string, mixed>
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public function interpret(EvidenceSnapshot $snapshot, ?EvidenceSnapshotItem $findingsItem): array
|
||||||
|
{
|
||||||
|
$findingsSummary = $this->findingsSummary($findingsItem);
|
||||||
|
$entries = $this->findingEntries($findingsSummary);
|
||||||
|
$unresolvedEntryCount = $entries
|
||||||
|
->filter(static fn (array $entry): bool => (string) data_get($entry, 'canonical_control_resolution.status') !== 'resolved')
|
||||||
|
->count();
|
||||||
|
$controls = $this->controlDefinitions($findingsSummary, $entries);
|
||||||
|
$snapshotLimitations = $this->snapshotLimitations($snapshot, $findingsItem, $unresolvedEntryCount);
|
||||||
|
|
||||||
|
$controlSummaries = $controls
|
||||||
|
->map(fn (CanonicalControlDefinition $definition): array => $this->controlSummary(
|
||||||
|
definition: $definition,
|
||||||
|
entries: $this->entriesForControl($entries, $definition->controlKey),
|
||||||
|
snapshotLimitations: $snapshotLimitations,
|
||||||
|
))
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
$globalLimitations = $this->globalLimitations($controlSummaries, $snapshotLimitations, $controls->isEmpty(), $unresolvedEntryCount);
|
||||||
|
$limitationCounts = $this->limitationCounts($controlSummaries, $globalLimitations);
|
||||||
|
|
||||||
|
$summary = [
|
||||||
|
'version_key' => self::VERSION_KEY,
|
||||||
|
'display_label' => 'Compliance evidence mapping v1',
|
||||||
|
'non_certification_disclosure' => 'TenantPilot interprets available evidence for review readiness. This is not a certification, legal attestation, or compliance guarantee.',
|
||||||
|
'mapped_control_count' => count($controlSummaries),
|
||||||
|
'follow_up_required_count' => collect($controlSummaries)
|
||||||
|
->where('readiness_bucket', 'follow_up_required')
|
||||||
|
->count(),
|
||||||
|
'limitation_counts' => $limitationCounts,
|
||||||
|
'limitations' => $globalLimitations,
|
||||||
|
'controls' => $controlSummaries,
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'summary' => $summary,
|
||||||
|
'section' => [
|
||||||
|
'section_key' => self::SECTION_KEY,
|
||||||
|
'title' => 'Control readiness interpretation',
|
||||||
|
'sort_order' => 15,
|
||||||
|
'required' => true,
|
||||||
|
'completeness_state' => $this->sectionCompleteness($findingsItem, $controls->isEmpty(), $snapshotLimitations),
|
||||||
|
'source_snapshot_fingerprint' => $this->sourceFingerprint($findingsItem) ?? (string) $snapshot->fingerprint,
|
||||||
|
'summary_payload' => Arr::except($summary, ['controls']),
|
||||||
|
'render_payload' => [
|
||||||
|
'entries' => array_map(
|
||||||
|
fn (array $control): array => $this->controlExplanation($control, $snapshot),
|
||||||
|
$controlSummaries,
|
||||||
|
),
|
||||||
|
'disclosure' => $summary['non_certification_disclosure'],
|
||||||
|
'next_actions' => $this->sectionNextActions($controlSummaries, $globalLimitations),
|
||||||
|
'empty_state' => $controlSummaries === []
|
||||||
|
? 'No canonical controls are mapped in this released review. Treat the control view as partial until evidence references can be mapped.'
|
||||||
|
: null,
|
||||||
|
],
|
||||||
|
'measured_at' => $findingsItem?->measured_at ?? $snapshot->generated_at,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function findingsSummary(?EvidenceSnapshotItem $findingsItem): array
|
||||||
|
{
|
||||||
|
return is_array($findingsItem?->summary_payload) ? $findingsItem->summary_payload : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $findingsSummary
|
||||||
|
* @return Collection<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function findingEntries(array $findingsSummary): Collection
|
||||||
|
{
|
||||||
|
return collect(Arr::wrap($findingsSummary['entries'] ?? []))
|
||||||
|
->filter(static fn (mixed $entry): bool => is_array($entry))
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $findingsSummary
|
||||||
|
* @param Collection<int, array<string, mixed>> $entries
|
||||||
|
* @return Collection<int, CanonicalControlDefinition>
|
||||||
|
*/
|
||||||
|
private function controlDefinitions(array $findingsSummary, Collection $entries): Collection
|
||||||
|
{
|
||||||
|
$summaryControls = collect(Arr::wrap($findingsSummary['canonical_controls'] ?? []))
|
||||||
|
->filter(static fn (mixed $control): bool => is_array($control));
|
||||||
|
|
||||||
|
$entryControls = $entries
|
||||||
|
->map(static fn (array $entry): mixed => data_get($entry, 'canonical_control_resolution.control'))
|
||||||
|
->filter(static fn (mixed $control): bool => is_array($control));
|
||||||
|
|
||||||
|
return $summaryControls
|
||||||
|
->merge($entryControls)
|
||||||
|
->map(fn (array $control): ?CanonicalControlDefinition => $this->definitionFor($control))
|
||||||
|
->filter()
|
||||||
|
->unique(static fn (CanonicalControlDefinition $definition): string => $definition->controlKey)
|
||||||
|
->sortBy(static fn (CanonicalControlDefinition $definition): string => $definition->name)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $control
|
||||||
|
*/
|
||||||
|
private function definitionFor(array $control): ?CanonicalControlDefinition
|
||||||
|
{
|
||||||
|
$controlKey = $control['control_key'] ?? null;
|
||||||
|
|
||||||
|
if (! is_string($controlKey) || trim($controlKey) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->catalog->find($controlKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, array<string, mixed>> $entries
|
||||||
|
* @return Collection<int, array<string, mixed>>
|
||||||
|
*/
|
||||||
|
private function entriesForControl(Collection $entries, string $controlKey): Collection
|
||||||
|
{
|
||||||
|
return $entries
|
||||||
|
->filter(static fn (array $entry): bool => (string) data_get($entry, 'canonical_control_resolution.control.control_key') === $controlKey)
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, array<string, mixed>> $entries
|
||||||
|
* @param list<string> $snapshotLimitations
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function controlSummary(CanonicalControlDefinition $definition, Collection $entries, array $snapshotLimitations): array
|
||||||
|
{
|
||||||
|
$openEntries = $entries->filter(static fn (array $entry): bool => in_array((string) ($entry['status'] ?? ''), Finding::openStatuses(), true));
|
||||||
|
$acceptedRiskEntries = $entries->filter(static fn (array $entry): bool => (string) ($entry['status'] ?? '') === Finding::STATUS_RISK_ACCEPTED);
|
||||||
|
$governanceWarnings = $entries->filter(static fn (array $entry): bool => self::hasGovernanceWarning($entry));
|
||||||
|
$limitationFlags = $this->controlLimitations($acceptedRiskEntries->count(), $snapshotLimitations);
|
||||||
|
$readinessBucket = $this->readinessBucket(
|
||||||
|
openCount: $openEntries->count(),
|
||||||
|
acceptedRiskCount: $acceptedRiskEntries->count(),
|
||||||
|
governanceWarningCount: $governanceWarnings->count(),
|
||||||
|
limitationFlags: $limitationFlags,
|
||||||
|
);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'control_key' => $definition->controlKey,
|
||||||
|
'control_name' => $definition->name,
|
||||||
|
'domain_key' => $definition->domainKey,
|
||||||
|
'readiness_bucket' => $readinessBucket,
|
||||||
|
'readiness_label' => self::readinessLabel($readinessBucket),
|
||||||
|
'limitation_flags' => $limitationFlags,
|
||||||
|
'limitation_labels' => array_map(self::limitationLabel(...), $limitationFlags),
|
||||||
|
'customer_summary' => $this->customerSummary($definition, $readinessBucket, $openEntries->count(), $acceptedRiskEntries->count()),
|
||||||
|
'evidence_basis_summary' => $this->evidenceBasisSummary($entries->count(), $openEntries->count(), $acceptedRiskEntries->count()),
|
||||||
|
'accepted_risk_summary' => $acceptedRiskEntries->isEmpty()
|
||||||
|
? null
|
||||||
|
: $this->acceptedRiskSummary($acceptedRiskEntries, $governanceWarnings->count()),
|
||||||
|
'recommended_next_action' => $this->recommendedNextAction($readinessBucket, $acceptedRiskEntries->count(), $limitationFlags),
|
||||||
|
'detail_anchor' => 'control-'.$definition->controlKey,
|
||||||
|
'supporting_finding_ids' => $entries
|
||||||
|
->pluck('id')
|
||||||
|
->filter(static fn (mixed $id): bool => is_numeric($id))
|
||||||
|
->map(static fn (mixed $id): int => (int) $id)
|
||||||
|
->values()
|
||||||
|
->all(),
|
||||||
|
'finding_count' => $entries->count(),
|
||||||
|
'open_finding_count' => $openEntries->count(),
|
||||||
|
'accepted_risk_count' => $acceptedRiskEntries->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Collection<int, array<string, mixed>> $acceptedRiskEntries
|
||||||
|
*/
|
||||||
|
private function acceptedRiskSummary(Collection $acceptedRiskEntries, int $governanceWarningCount): string
|
||||||
|
{
|
||||||
|
if ($governanceWarningCount > 0) {
|
||||||
|
return sprintf(
|
||||||
|
'%d accepted-risk finding(s) need governance follow-up before relying on this interpretation.',
|
||||||
|
$acceptedRiskEntries->count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sprintf(
|
||||||
|
'%d accepted-risk finding(s) are part of the evidence basis and qualify the readiness view.',
|
||||||
|
$acceptedRiskEntries->count(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $snapshotLimitations
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function controlLimitations(int $acceptedRiskCount, array $snapshotLimitations): array
|
||||||
|
{
|
||||||
|
$limitations = $snapshotLimitations;
|
||||||
|
|
||||||
|
if ($acceptedRiskCount > 0) {
|
||||||
|
$limitations[] = 'accepted_risk_influenced';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($limitations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $limitationFlags
|
||||||
|
*/
|
||||||
|
private function readinessBucket(int $openCount, int $acceptedRiskCount, int $governanceWarningCount, array $limitationFlags): string
|
||||||
|
{
|
||||||
|
if ($openCount > 0 || $governanceWarningCount > 0) {
|
||||||
|
return 'follow_up_required';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($acceptedRiskCount > 0 || $limitationFlags !== []) {
|
||||||
|
return 'review_recommended';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'evidence_on_record';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function customerSummary(CanonicalControlDefinition $definition, string $readinessBucket, int $openCount, int $acceptedRiskCount): string
|
||||||
|
{
|
||||||
|
return match ($readinessBucket) {
|
||||||
|
'follow_up_required' => sprintf(
|
||||||
|
'%s needs follow-up because %d open finding(s) remain in the released evidence basis.',
|
||||||
|
$definition->name,
|
||||||
|
$openCount,
|
||||||
|
),
|
||||||
|
'review_recommended' => $acceptedRiskCount > 0
|
||||||
|
? sprintf('%s has evidence on record with accepted-risk context that should be reviewed before relying on the interpretation.', $definition->name)
|
||||||
|
: sprintf('%s has evidence on record, with limitations that should be reviewed before relying on the interpretation.', $definition->name),
|
||||||
|
default => sprintf('%s has evidence on record in this released review.', $definition->name),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evidenceBasisSummary(int $signalCount, int $openCount, int $acceptedRiskCount): string
|
||||||
|
{
|
||||||
|
$parts = [
|
||||||
|
sprintf('%d evidence signal(s) reference this control.', $signalCount),
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($openCount > 0) {
|
||||||
|
$parts[] = sprintf('%d open finding(s) still need follow-up.', $openCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($acceptedRiskCount > 0) {
|
||||||
|
$parts[] = sprintf('%d accepted-risk finding(s) qualify this view.', $acceptedRiskCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
return implode(' ', $parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $limitationFlags
|
||||||
|
*/
|
||||||
|
private function recommendedNextAction(string $readinessBucket, int $acceptedRiskCount, array $limitationFlags): string
|
||||||
|
{
|
||||||
|
if ($readinessBucket === 'follow_up_required') {
|
||||||
|
return 'Review the surfaced findings with the tenant and agree ownership plus follow-up timing.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($acceptedRiskCount > 0) {
|
||||||
|
return 'Review the accepted-risk owner and next review date before customer delivery.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($limitationFlags !== []) {
|
||||||
|
return 'Confirm the evidence basis and limitations before using this control as customer-facing readiness support.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Keep this evidence on record and revisit it during the normal review cadence.';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $control
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function controlExplanation(array $control, EvidenceSnapshot $snapshot): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'title' => $control['control_name'],
|
||||||
|
'control_key' => $control['control_key'],
|
||||||
|
'control_name' => $control['control_name'],
|
||||||
|
'readiness_bucket' => $control['readiness_bucket'],
|
||||||
|
'readiness_label' => $control['readiness_label'],
|
||||||
|
'limitation_flags' => $control['limitation_flags'],
|
||||||
|
'limitation_labels' => $control['limitation_labels'],
|
||||||
|
'customer_summary' => $control['customer_summary'],
|
||||||
|
'evidence_basis_summary' => $control['evidence_basis_summary'],
|
||||||
|
'accepted_risk_summary' => $control['accepted_risk_summary'],
|
||||||
|
'explanation_text' => $control['customer_summary'],
|
||||||
|
'evidence_basis_items' => array_values(array_filter([
|
||||||
|
$control['evidence_basis_summary'],
|
||||||
|
$control['accepted_risk_summary'],
|
||||||
|
])),
|
||||||
|
'accepted_risk_context' => $control['accepted_risk_summary'],
|
||||||
|
'recommended_next_action' => $control['recommended_next_action'],
|
||||||
|
'proof_access_state' => $this->proofAccessState($snapshot),
|
||||||
|
'supporting_finding_ids' => $control['supporting_finding_ids'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $controlSummaries
|
||||||
|
* @param list<string> $globalLimitations
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function sectionNextActions(array $controlSummaries, array $globalLimitations): array
|
||||||
|
{
|
||||||
|
if ($controlSummaries === []) {
|
||||||
|
return ['Review unmapped evidence before using this review for customer-facing readiness discussions.'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$actions = collect($controlSummaries)
|
||||||
|
->pluck('recommended_next_action')
|
||||||
|
->filter(static fn (mixed $action): bool => is_string($action) && trim($action) !== '')
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if (in_array('unmapped', $globalLimitations, true)) {
|
||||||
|
$actions[] = 'Treat this review as partial until unmapped evidence can be interpreted.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($actions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $controlSummaries
|
||||||
|
* @param list<string> $snapshotLimitations
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function globalLimitations(array $controlSummaries, array $snapshotLimitations, bool $noMappedControls, int $unresolvedEntryCount): array
|
||||||
|
{
|
||||||
|
$limitations = $snapshotLimitations;
|
||||||
|
|
||||||
|
if ($noMappedControls) {
|
||||||
|
$limitations[] = 'unmapped';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unresolvedEntryCount > 0) {
|
||||||
|
$limitations[] = 'partial_mapping';
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($controlSummaries as $control) {
|
||||||
|
foreach (Arr::wrap($control['limitation_flags'] ?? []) as $limitation) {
|
||||||
|
if (is_string($limitation) && trim($limitation) !== '') {
|
||||||
|
$limitations[] = $limitation;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($limitations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $controlSummaries
|
||||||
|
* @param list<string> $globalLimitations
|
||||||
|
* @return array<string, int>
|
||||||
|
*/
|
||||||
|
private function limitationCounts(array $controlSummaries, array $globalLimitations): array
|
||||||
|
{
|
||||||
|
$counts = collect($controlSummaries)
|
||||||
|
->flatMap(static fn (array $control): array => Arr::wrap($control['limitation_flags'] ?? []))
|
||||||
|
->filter(static fn (mixed $limitation): bool => is_string($limitation) && trim($limitation) !== '')
|
||||||
|
->countBy()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($globalLimitations as $limitation) {
|
||||||
|
$counts[$limitation] = max((int) ($counts[$limitation] ?? 0), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($counts);
|
||||||
|
|
||||||
|
return array_map('intval', $counts);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function snapshotLimitations(EvidenceSnapshot $snapshot, ?EvidenceSnapshotItem $findingsItem, int $unresolvedEntryCount): array
|
||||||
|
{
|
||||||
|
$limitations = [];
|
||||||
|
$state = (string) ($findingsItem?->state ?? $snapshot->completeness_state);
|
||||||
|
|
||||||
|
if ($state === TenantReviewCompletenessState::Stale->value || (string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
||||||
|
$limitations[] = 'stale_evidence';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($state, [TenantReviewCompletenessState::Partial->value, TenantReviewCompletenessState::Missing->value], true)) {
|
||||||
|
$limitations[] = 'partial_mapping';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($unresolvedEntryCount > 0) {
|
||||||
|
$limitations[] = 'partial_mapping';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $snapshot->exists || $snapshot->generated_at === null) {
|
||||||
|
$limitations[] = 'supporting_evidence_unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($limitations));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $snapshotLimitations
|
||||||
|
*/
|
||||||
|
private function sectionCompleteness(?EvidenceSnapshotItem $findingsItem, bool $noMappedControls, array $snapshotLimitations): string
|
||||||
|
{
|
||||||
|
if (! $findingsItem instanceof EvidenceSnapshotItem) {
|
||||||
|
return TenantReviewCompletenessState::Missing->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array('stale_evidence', $snapshotLimitations, true)) {
|
||||||
|
return TenantReviewCompletenessState::Stale->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($noMappedControls || in_array('partial_mapping', $snapshotLimitations, true)) {
|
||||||
|
return TenantReviewCompletenessState::Partial->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TenantReviewCompletenessState::tryFrom((string) $findingsItem->state)?->value
|
||||||
|
?? TenantReviewCompletenessState::Missing->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function proofAccessState(EvidenceSnapshot $snapshot): string
|
||||||
|
{
|
||||||
|
if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
||||||
|
return 'expired';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $snapshot->exists || $snapshot->generated_at === null) {
|
||||||
|
return 'unavailable';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'available';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sourceFingerprint(?EvidenceSnapshotItem $item): ?string
|
||||||
|
{
|
||||||
|
$fingerprint = $item?->source_fingerprint;
|
||||||
|
|
||||||
|
return is_string($fingerprint) && $fingerprint !== '' ? $fingerprint : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $entry
|
||||||
|
*/
|
||||||
|
private static function hasGovernanceWarning(array $entry): bool
|
||||||
|
{
|
||||||
|
if (is_string($entry['governance_warning'] ?? null) && trim((string) $entry['governance_warning']) !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array((string) ($entry['governance_state'] ?? ''), [
|
||||||
|
'expired_exception',
|
||||||
|
'revoked_exception',
|
||||||
|
'rejected_exception',
|
||||||
|
'risk_accepted_without_valid_exception',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function readinessLabel(string $bucket): string
|
||||||
|
{
|
||||||
|
return match ($bucket) {
|
||||||
|
'follow_up_required' => 'Follow-up required',
|
||||||
|
'review_recommended' => 'Review recommended',
|
||||||
|
'evidence_on_record' => 'Evidence on record',
|
||||||
|
default => Str::headline($bucket),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function limitationLabel(string $flag): string
|
||||||
|
{
|
||||||
|
return match ($flag) {
|
||||||
|
'accepted_risk_influenced' => 'Accepted risk influences this view',
|
||||||
|
'partial_mapping' => 'Partial evidence mapping',
|
||||||
|
'stale_evidence' => 'Evidence freshness needs review',
|
||||||
|
'supporting_evidence_unavailable' => 'Supporting evidence unavailable',
|
||||||
|
'unmapped' => 'No mapped control coverage',
|
||||||
|
default => Str::headline($flag),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -132,17 +132,38 @@
|
|||||||
'customer_safe_review_workspace' => 'Kundensicherer Review-Workspace',
|
'customer_safe_review_workspace' => 'Kundensicherer Review-Workspace',
|
||||||
'customer_workspace_intro' => 'Prüfen Sie den zuletzt veröffentlichten kundensicheren Status für jeden berechtigten Tenant, ohne den aktuellen Workspace-Kontext zu verlassen.',
|
'customer_workspace_intro' => 'Prüfen Sie den zuletzt veröffentlichten kundensicheren Status für jeden berechtigten Tenant, ohne den aktuellen Workspace-Kontext zu verlassen.',
|
||||||
'customer_workspace_canonical_note' => 'Eine Zeile öffnet die bestehende Tenant-Review-Detailseite, damit Evidence, Review-Packs und auditfähige Nachweise auf ihren kanonischen tenantbezogenen Oberflächen bleiben.',
|
'customer_workspace_canonical_note' => 'Eine Zeile öffnet die bestehende Tenant-Review-Detailseite, damit Evidence, Review-Packs und auditfähige Nachweise auf ihren kanonischen tenantbezogenen Oberflächen bleiben.',
|
||||||
|
'customer_workspace_mapping_version' => 'Die Control-Readiness-Interpretation verwendet :version für diesen Workspace.',
|
||||||
|
'customer_workspace_non_certification_disclosure' => 'TenantPilot interpretiert verfügbare Evidence für Review-Readiness. Dies ist keine Zertifizierung, rechtliche Attestierung oder Compliance-Garantie.',
|
||||||
'reviews' => 'Reviews',
|
'reviews' => 'Reviews',
|
||||||
'clear_filters' => 'Filter löschen',
|
'clear_filters' => 'Filter löschen',
|
||||||
'tenant' => 'Tenant',
|
'tenant' => 'Tenant',
|
||||||
'latest_review' => 'Letztes Review',
|
'latest_review' => 'Letztes Review',
|
||||||
|
'control' => 'Control',
|
||||||
|
'control_interpretation' => 'Control-Readiness-Interpretation',
|
||||||
|
'control_readiness' => 'Control-Readiness',
|
||||||
|
'review_recommended' => 'Review empfohlen',
|
||||||
|
'recommended_next_action' => 'Empfohlene nächste Aktion',
|
||||||
|
'customer_safe' => 'Kundensicher',
|
||||||
|
'interpretation_version_short' => 'Interpretationsversion: :version',
|
||||||
|
'additional_controls' => '+:count weitere Control(s)',
|
||||||
|
'control_limitations_summary' => 'Limitierungen: :limitations.',
|
||||||
|
'control_readiness_unmapped' => 'Keine gemappten Controls',
|
||||||
|
'control_readiness_unmapped_description' => 'In diesem veröffentlichten Review sind keine kanonischen Controls gemappt. Behandeln Sie die Control-Sicht als partiell, bis Evidence-Referenzen gemappt werden können.',
|
||||||
|
'control_evidence_unmapped' => 'Keine gemappte Evidence-Basis verfügbar.',
|
||||||
|
'control_evidence_unavailable' => 'Evidence-Basis nicht verfügbar.',
|
||||||
|
'control_recommendation_unmapped' => 'Prüfen Sie unmapped Evidence vor der Kundenauslieferung.',
|
||||||
|
'proof_access_state' => 'Proof-Zugriff',
|
||||||
'key_findings' => 'Wichtige Findings',
|
'key_findings' => 'Wichtige Findings',
|
||||||
'accepted_risks' => 'Akzeptierte Risiken',
|
'accepted_risks' => 'Akzeptierte Risiken',
|
||||||
|
'evidence_proof' => 'Evidence-Nachweis',
|
||||||
'published' => 'Veröffentlicht',
|
'published' => 'Veröffentlicht',
|
||||||
'review_pack' => 'Review-Pack',
|
'review_pack' => 'Review-Pack',
|
||||||
'open_latest_review' => 'Letztes Review öffnen',
|
'open_latest_review' => 'Letztes Review öffnen',
|
||||||
'download_review_pack' => 'Review-Pack herunterladen',
|
'download_review_pack' => 'Review-Pack herunterladen',
|
||||||
|
'download_current_review_pack' => 'Aktuelles Review-Pack herunterladen',
|
||||||
'no_entitled_tenants' => 'Keine berechtigten Tenants passen zu dieser Ansicht',
|
'no_entitled_tenants' => 'Keine berechtigten Tenants passen zu dieser Ansicht',
|
||||||
|
'no_released_customer_reviews' => 'Keine veröffentlichten Kundenreviews passen zu dieser Ansicht',
|
||||||
|
'no_released_customer_reviews_description' => 'Veröffentlichen Sie ein Tenant-Review, bevor es im kundensicheren Workspace erscheint.',
|
||||||
'clear_filters_description' => 'Löschen Sie die aktuellen Filter, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
|
'clear_filters_description' => 'Löschen Sie die aktuellen Filter, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
|
||||||
'adjust_filters_description' => 'Passen Sie die Filter an, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
|
'adjust_filters_description' => 'Passen Sie die Filter an, um zum vollständigen Kundenreview-Workspace für Ihre berechtigten Tenants zurückzukehren.',
|
||||||
'no_published_review' => 'Kein veröffentlichtes Review',
|
'no_published_review' => 'Kein veröffentlichtes Review',
|
||||||
@ -154,8 +175,28 @@
|
|||||||
'accepted_risks_need_follow_up' => ':warnings akzeptierte Risiken benötigen Governance-Nacharbeit (:total gesamt).',
|
'accepted_risks_need_follow_up' => ':warnings akzeptierte Risiken benötigen Governance-Nacharbeit (:total gesamt).',
|
||||||
'accepted_risks_governed' => ':count akzeptierte Risiken sind governed.',
|
'accepted_risks_governed' => ':count akzeptierte Risiken sind governed.',
|
||||||
'accepted_risks_on_record' => ':count akzeptierte Risiken sind erfasst.',
|
'accepted_risks_on_record' => ':count akzeptierte Risiken sind erfasst.',
|
||||||
|
'accepted_risk_accountable' => 'Verantwortlich: :name.',
|
||||||
|
'accepted_risk_accountable_until' => 'Verantwortlich: :name. Erneute Prüfung bis :date.',
|
||||||
|
'accepted_risk_reason' => 'Begründung: :reason.',
|
||||||
|
'accepted_risk_partial_accountability' => 'Die Verantwortlichkeit ist teilweise erfasst; Review-Owner-Details sind nicht vollständig verfügbar.',
|
||||||
'unavailable' => 'Nicht verfügbar',
|
'unavailable' => 'Nicht verfügbar',
|
||||||
'available' => 'Verfügbar',
|
'available' => 'Verfügbar',
|
||||||
|
'review_pack_available' => 'Aktuelles Review-Pack verfügbar',
|
||||||
|
'no_current_review_pack' => 'Noch kein aktuelles Review-Pack verfügbar',
|
||||||
|
'review_pack_access_unavailable' => 'Review-Pack-Zugriff ist für dieses Konto nicht verfügbar',
|
||||||
|
'review_pack_unavailable' => 'Review-Pack ist noch nicht bereit',
|
||||||
|
'review_pack_expired' => 'Review-Pack abgelaufen',
|
||||||
|
'evidence_proof_available' => 'Nachweiszusammenfassung verfügbar',
|
||||||
|
'evidence_proof_absent' => 'Noch keine Nachweiszusammenfassung verknüpft',
|
||||||
|
'evidence_proof_access_unavailable' => 'Nachweiszugriff ist für dieses Konto nicht verfügbar',
|
||||||
|
'evidence_proof_expired' => 'Nachweiszusammenfassung abgelaufen',
|
||||||
|
'customer_review_pack_unavailable' => 'Das aktuelle Review-Pack kann aus diesem kundensicheren Flow nicht heruntergeladen werden.',
|
||||||
|
'customer_review_pack_missing' => 'Diesem veröffentlichten Review ist noch kein aktuelles Review-Pack zugeordnet.',
|
||||||
|
'customer_review_pack_not_ready' => 'Das zugeordnete Review-Pack ist noch nicht für den Download bereit.',
|
||||||
|
'customer_review_pack_expired' => 'Das zugeordnete Review-Pack ist abgelaufen.',
|
||||||
|
'customer_review_pack_forbidden' => 'Dieses Konto kann das Review lesen, aber das aktuelle Review-Pack nicht herunterladen.',
|
||||||
|
'released_governance_record' => 'Veröffentlichter Governance-Nachweis',
|
||||||
|
'released_governance_record_available' => 'Dieses veröffentlichte Review ist für kundensichere Governance-Nutzung verfügbar.',
|
||||||
'outcome_summary' => 'Ergebniszusammenfassung',
|
'outcome_summary' => 'Ergebniszusammenfassung',
|
||||||
'review' => 'Review',
|
'review' => 'Review',
|
||||||
'review_date' => 'Review-Datum',
|
'review_date' => 'Review-Datum',
|
||||||
|
|||||||
@ -132,17 +132,38 @@
|
|||||||
'customer_safe_review_workspace' => 'Customer-safe review workspace',
|
'customer_safe_review_workspace' => 'Customer-safe review workspace',
|
||||||
'customer_workspace_intro' => 'Review the latest published customer-safe posture for each entitled tenant without leaving the current workspace context.',
|
'customer_workspace_intro' => 'Review the latest published customer-safe posture for each entitled tenant without leaving the current workspace context.',
|
||||||
'customer_workspace_canonical_note' => 'Opening a row returns to the existing tenant review detail so evidence, review packs, and audit-aware proof remain on their canonical tenant-scoped surfaces.',
|
'customer_workspace_canonical_note' => 'Opening a row returns to the existing tenant review detail so evidence, review packs, and audit-aware proof remain on their canonical tenant-scoped surfaces.',
|
||||||
|
'customer_workspace_mapping_version' => 'Control readiness interpretation uses :version for this workspace.',
|
||||||
|
'customer_workspace_non_certification_disclosure' => 'TenantPilot interprets available evidence for review readiness. This is not a certification, legal attestation, or compliance guarantee.',
|
||||||
'reviews' => 'Reviews',
|
'reviews' => 'Reviews',
|
||||||
'clear_filters' => 'Clear filters',
|
'clear_filters' => 'Clear filters',
|
||||||
'tenant' => 'Tenant',
|
'tenant' => 'Tenant',
|
||||||
'latest_review' => 'Latest review',
|
'latest_review' => 'Latest review',
|
||||||
|
'control' => 'Control',
|
||||||
|
'control_interpretation' => 'Control readiness interpretation',
|
||||||
|
'control_readiness' => 'Control readiness',
|
||||||
|
'review_recommended' => 'Review recommended',
|
||||||
|
'recommended_next_action' => 'Recommended next action',
|
||||||
|
'customer_safe' => 'Customer-safe',
|
||||||
|
'interpretation_version_short' => 'Interpretation version: :version',
|
||||||
|
'additional_controls' => '+:count more control(s)',
|
||||||
|
'control_limitations_summary' => 'Limitations: :limitations.',
|
||||||
|
'control_readiness_unmapped' => 'No mapped controls',
|
||||||
|
'control_readiness_unmapped_description' => 'No canonical controls are mapped in this released review. Treat the control view as partial until evidence references can be mapped.',
|
||||||
|
'control_evidence_unmapped' => 'No mapped evidence basis is available.',
|
||||||
|
'control_evidence_unavailable' => 'Evidence basis unavailable.',
|
||||||
|
'control_recommendation_unmapped' => 'Review unmapped evidence before customer delivery.',
|
||||||
|
'proof_access_state' => 'Proof access',
|
||||||
'key_findings' => 'Key findings',
|
'key_findings' => 'Key findings',
|
||||||
'accepted_risks' => 'Accepted risks',
|
'accepted_risks' => 'Accepted risks',
|
||||||
|
'evidence_proof' => 'Evidence proof',
|
||||||
'published' => 'Published',
|
'published' => 'Published',
|
||||||
'review_pack' => 'Review pack',
|
'review_pack' => 'Review pack',
|
||||||
'open_latest_review' => 'Open latest review',
|
'open_latest_review' => 'Open latest review',
|
||||||
'download_review_pack' => 'Download review pack',
|
'download_review_pack' => 'Download review pack',
|
||||||
|
'download_current_review_pack' => 'Download current review pack',
|
||||||
'no_entitled_tenants' => 'No entitled tenants match this view',
|
'no_entitled_tenants' => 'No entitled tenants match this view',
|
||||||
|
'no_released_customer_reviews' => 'No released customer reviews match this view',
|
||||||
|
'no_released_customer_reviews_description' => 'Publish a tenant review before it appears in the customer-safe workspace.',
|
||||||
'clear_filters_description' => 'Clear the current filters to return to the full customer review workspace for your entitled tenants.',
|
'clear_filters_description' => 'Clear the current filters to return to the full customer review workspace for your entitled tenants.',
|
||||||
'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled tenants.',
|
'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled tenants.',
|
||||||
'no_published_review' => 'No published review',
|
'no_published_review' => 'No published review',
|
||||||
@ -154,8 +175,28 @@
|
|||||||
'accepted_risks_need_follow_up' => ':warnings accepted risks need governance follow-up (:total total).',
|
'accepted_risks_need_follow_up' => ':warnings accepted risks need governance follow-up (:total total).',
|
||||||
'accepted_risks_governed' => ':count accepted risks are governed.',
|
'accepted_risks_governed' => ':count accepted risks are governed.',
|
||||||
'accepted_risks_on_record' => ':count accepted risks are on record.',
|
'accepted_risks_on_record' => ':count accepted risks are on record.',
|
||||||
|
'accepted_risk_accountable' => 'Accountable: :name.',
|
||||||
|
'accepted_risk_accountable_until' => 'Accountable: :name. Re-review by :date.',
|
||||||
|
'accepted_risk_reason' => 'Reason: :reason.',
|
||||||
|
'accepted_risk_partial_accountability' => 'Accountability is partially recorded; review owner details are not fully available.',
|
||||||
'unavailable' => 'Unavailable',
|
'unavailable' => 'Unavailable',
|
||||||
'available' => 'Available',
|
'available' => 'Available',
|
||||||
|
'review_pack_available' => 'Current review pack available',
|
||||||
|
'no_current_review_pack' => 'No current review pack available yet',
|
||||||
|
'review_pack_access_unavailable' => 'Review pack access is unavailable for this actor',
|
||||||
|
'review_pack_unavailable' => 'Review pack is not ready yet',
|
||||||
|
'review_pack_expired' => 'Review pack expired',
|
||||||
|
'evidence_proof_available' => 'Proof summary available',
|
||||||
|
'evidence_proof_absent' => 'No proof summary linked yet',
|
||||||
|
'evidence_proof_access_unavailable' => 'Proof access is unavailable for this actor',
|
||||||
|
'evidence_proof_expired' => 'Proof summary expired',
|
||||||
|
'customer_review_pack_unavailable' => 'The current review pack cannot be downloaded from this customer-safe flow.',
|
||||||
|
'customer_review_pack_missing' => 'No current review pack is attached to this released review yet.',
|
||||||
|
'customer_review_pack_not_ready' => 'The attached review pack is not ready for download yet.',
|
||||||
|
'customer_review_pack_expired' => 'The attached review pack has expired.',
|
||||||
|
'customer_review_pack_forbidden' => 'This account can read the review but cannot download the current review pack.',
|
||||||
|
'released_governance_record' => 'Released governance record',
|
||||||
|
'released_governance_record_available' => 'This released review is available for customer-safe governance consumption.',
|
||||||
'outcome_summary' => 'Outcome summary',
|
'outcome_summary' => 'Outcome summary',
|
||||||
'review' => 'Review',
|
'review' => 'Review',
|
||||||
'review_date' => 'Review date',
|
'review_date' => 'Review date',
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
$links = is_array($state['links'] ?? null) ? $state['links'] : [];
|
$links = is_array($state['links'] ?? null) ? $state['links'] : [];
|
||||||
$disclosure = is_string($state['disclosure'] ?? null) ? $state['disclosure'] : null;
|
$disclosure = is_string($state['disclosure'] ?? null) ? $state['disclosure'] : null;
|
||||||
$emptyState = is_string($state['empty_state'] ?? null) ? $state['empty_state'] : null;
|
$emptyState = is_string($state['empty_state'] ?? null) ? $state['empty_state'] : null;
|
||||||
|
$isControlInterpretation = (bool) ($state['is_control_interpretation'] ?? false);
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
@ -48,6 +49,72 @@
|
|||||||
@continue(! is_array($entry))
|
@continue(! is_array($entry))
|
||||||
|
|
||||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/60">
|
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-3 text-sm dark:border-gray-800 dark:bg-gray-950/60">
|
||||||
|
@if ($isControlInterpretation)
|
||||||
|
@php
|
||||||
|
$readinessBucket = is_string($entry['readiness_bucket'] ?? null) ? $entry['readiness_bucket'] : 'review_recommended';
|
||||||
|
$readinessColor = match ($readinessBucket) {
|
||||||
|
'follow_up_required' => 'warning',
|
||||||
|
'review_recommended' => 'info',
|
||||||
|
'evidence_on_record' => 'success',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
$limitationLabels = is_array($entry['limitation_labels'] ?? null) ? $entry['limitation_labels'] : [];
|
||||||
|
$basisItems = is_array($entry['evidence_basis_items'] ?? null) ? $entry['evidence_basis_items'] : [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ $entry['control_name'] ?? $entry['title'] ?? __('localization.review.control') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-filament::badge :color="$readinessColor" size="sm">
|
||||||
|
{{ $entry['readiness_label'] ?? __('localization.review.review_recommended') }}
|
||||||
|
</x-filament::badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (filled($entry['explanation_text'] ?? null))
|
||||||
|
<div class="mt-2 text-gray-700 dark:text-gray-300">
|
||||||
|
{{ $entry['explanation_text'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($basisItems !== [])
|
||||||
|
<div class="mt-3 space-y-1">
|
||||||
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.evidence_basis') }}</div>
|
||||||
|
<ul class="space-y-1 text-gray-700 dark:text-gray-300">
|
||||||
|
@foreach ($basisItems as $basisItem)
|
||||||
|
@continue(! is_string($basisItem) || trim($basisItem) === '')
|
||||||
|
|
||||||
|
<li>{{ $basisItem }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (filled($entry['recommended_next_action'] ?? null))
|
||||||
|
<div class="mt-3 rounded-md border border-primary-100 bg-primary-50 px-3 py-2 text-primary-900 dark:border-primary-900/40 dark:bg-primary-950/30 dark:text-primary-100">
|
||||||
|
{{ $entry['recommended_next_action'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($limitationLabels !== [])
|
||||||
|
<div class="mt-3 flex flex-wrap gap-1">
|
||||||
|
@foreach ($limitationLabels as $label)
|
||||||
|
@continue(! is_string($label) || trim($label) === '')
|
||||||
|
|
||||||
|
<x-filament::badge color="gray" size="sm">
|
||||||
|
{{ $label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (filled($entry['proof_access_state'] ?? null))
|
||||||
|
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('localization.review.proof_access_state') }}: {{ \Illuminate\Support\Str::headline((string) $entry['proof_access_state']) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@else
|
||||||
<div class="font-medium text-gray-900 dark:text-gray-100">
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
{{ $entry['title'] ?? $entry['displayName'] ?? $entry['type'] ?? __('localization.review.entry') }}
|
{{ $entry['title'] ?? $entry['displayName'] ?? $entry['type'] ?? __('localization.review.entry') }}
|
||||||
</div>
|
</div>
|
||||||
@ -64,6 +131,7 @@
|
|||||||
@if ($detailParts !== [])
|
@if ($detailParts !== [])
|
||||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ implode(' · ', $detailParts) }}</div>
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ implode(' · ', $detailParts) }}</div>
|
||||||
@endif
|
@endif
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -10,6 +10,14 @@
|
|||||||
$operatorExplanation = is_array($state['operator_explanation'] ?? null) ? $state['operator_explanation'] : [];
|
$operatorExplanation = is_array($state['operator_explanation'] ?? null) ? $state['operator_explanation'] : [];
|
||||||
$compressedOutcome = is_array($state['compressed_outcome'] ?? null) ? $state['compressed_outcome'] : [];
|
$compressedOutcome = is_array($state['compressed_outcome'] ?? null) ? $state['compressed_outcome'] : [];
|
||||||
$reasonSemantics = is_array($state['reason_semantics'] ?? null) ? $state['reason_semantics'] : [];
|
$reasonSemantics = is_array($state['reason_semantics'] ?? null) ? $state['reason_semantics'] : [];
|
||||||
|
$customerWorkspaceMode = (bool) ($state['customer_workspace_mode'] ?? false);
|
||||||
|
$controlInterpretation = is_array($state['control_interpretation'] ?? null) ? $state['control_interpretation'] : [];
|
||||||
|
$controlControls = is_array($controlInterpretation['controls'] ?? null) ? $controlInterpretation['controls'] : [];
|
||||||
|
$controlVersion = is_string($controlInterpretation['version_key'] ?? null) ? $controlInterpretation['version_key'] : null;
|
||||||
|
$controlDisclosure = is_string($controlInterpretation['non_certification_disclosure'] ?? null)
|
||||||
|
? $controlInterpretation['non_certification_disclosure']
|
||||||
|
: null;
|
||||||
|
$controlLimitations = is_array($controlInterpretation['limitations'] ?? null) ? $controlInterpretation['limitations'] : [];
|
||||||
$decisionDirection = is_string($compressedOutcome['decisionDirection'] ?? null)
|
$decisionDirection = is_string($compressedOutcome['decisionDirection'] ?? null)
|
||||||
? trim((string) $compressedOutcome['decisionDirection'])
|
? trim((string) $compressedOutcome['decisionDirection'])
|
||||||
: null;
|
: null;
|
||||||
@ -72,6 +80,97 @@
|
|||||||
@endforeach
|
@endforeach
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
|
@if ($customerWorkspaceMode && $controlInterpretation !== [])
|
||||||
|
<div class="space-y-3 rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
||||||
|
<div class="flex flex-wrap items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<div class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||||
|
{{ $controlInterpretation['display_label'] ?? __('localization.review.control_interpretation') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($controlVersion !== null)
|
||||||
|
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('localization.review.interpretation_version_short', ['version' => $controlVersion]) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-filament::badge color="gray" size="sm">
|
||||||
|
{{ __('localization.review.customer_safe') }}
|
||||||
|
</x-filament::badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($controlDisclosure !== null)
|
||||||
|
<div class="rounded-md border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-200">
|
||||||
|
{{ $controlDisclosure }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($controlControls !== [])
|
||||||
|
<div class="space-y-2">
|
||||||
|
@foreach ($controlControls as $control)
|
||||||
|
@php
|
||||||
|
$readinessBucket = is_string($control['readiness_bucket'] ?? null) ? $control['readiness_bucket'] : 'review_recommended';
|
||||||
|
$readinessColor = match ($readinessBucket) {
|
||||||
|
'follow_up_required' => 'warning',
|
||||||
|
'review_recommended' => 'info',
|
||||||
|
'evidence_on_record' => 'success',
|
||||||
|
default => 'gray',
|
||||||
|
};
|
||||||
|
$limitationLabels = is_array($control['limitation_labels'] ?? null) ? $control['limitation_labels'] : [];
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-3 dark:border-gray-800 dark:bg-gray-950/60">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="font-medium text-gray-900 dark:text-gray-100">
|
||||||
|
{{ $control['control_name'] ?? __('localization.review.control') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<x-filament::badge :color="$readinessColor" size="sm">
|
||||||
|
{{ $control['readiness_label'] ?? __('localization.review.review_recommended') }}
|
||||||
|
</x-filament::badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (filled($control['customer_summary'] ?? null))
|
||||||
|
<div class="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
{{ $control['customer_summary'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if (filled($control['evidence_basis_summary'] ?? null))
|
||||||
|
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $control['evidence_basis_summary'] }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($limitationLabels !== [])
|
||||||
|
<div class="mt-2 flex flex-wrap gap-1">
|
||||||
|
@foreach ($limitationLabels as $label)
|
||||||
|
@continue(! is_string($label) || trim($label) === '')
|
||||||
|
|
||||||
|
<x-filament::badge color="gray" size="sm">
|
||||||
|
{{ $label }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 text-sm text-gray-700 dark:border-gray-800 dark:bg-gray-950/60 dark:text-gray-300">
|
||||||
|
{{ __('localization.review.control_readiness_unmapped_description') }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($controlLimitations !== [])
|
||||||
|
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('localization.review.control_limitations_summary', ['limitations' => implode(', ', array_map(fn (string $flag): string => \App\Support\Governance\Controls\ComplianceEvidenceMappingV1::limitationLabel($flag), array_filter($controlLimitations, 'is_string')))]) }}
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if ($highlights !== [])
|
@if ($highlights !== [])
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.highlights') }}</div>
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.highlights') }}</div>
|
||||||
@ -110,14 +209,20 @@
|
|||||||
$description = is_string($link['description'] ?? null) ? $link['description'] : null;
|
$description = is_string($link['description'] ?? null) ? $link['description'] : null;
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@continue($title === null || $label === null || $url === null)
|
@continue($title === null || $label === null)
|
||||||
|
|
||||||
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 shadow-sm dark:border-gray-800 dark:bg-gray-900/70">
|
||||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $title }}</div>
|
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ $title }}</div>
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
|
@if ($url !== null)
|
||||||
<x-filament::link :href="$url" icon="heroicon-m-arrow-top-right-on-square">
|
<x-filament::link :href="$url" icon="heroicon-m-arrow-top-right-on-square">
|
||||||
{{ $label }}
|
{{ $label }}
|
||||||
</x-filament::link>
|
</x-filament::link>
|
||||||
|
@else
|
||||||
|
<x-filament::badge color="gray" size="sm">
|
||||||
|
{{ __('localization.review.unavailable') }}
|
||||||
|
</x-filament::badge>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if ($description !== null && trim($description) !== '')
|
@if ($description !== null && trim($description) !== '')
|
||||||
@ -130,9 +235,15 @@
|
|||||||
@endif
|
@endif
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.publication_readiness') }}</div>
|
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||||
|
{{ $customerWorkspaceMode ? __('localization.review.released_governance_record') : __('localization.review.publication_readiness') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
@if ($publishBlockers === [] && $decisionDirection === 'publishable')
|
@if ($customerWorkspaceMode)
|
||||||
|
<div class="rounded-md border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800 dark:border-emerald-900/40 dark:bg-emerald-950/30 dark:text-emerald-200">
|
||||||
|
{{ __('localization.review.released_governance_record_available') }}
|
||||||
|
</div>
|
||||||
|
@elseif ($publishBlockers === [] && $decisionDirection === 'publishable')
|
||||||
<div class="rounded-md border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800 dark:border-emerald-900/40 dark:bg-emerald-950/30 dark:text-emerald-200">
|
<div class="rounded-md border border-emerald-100 bg-emerald-50 px-3 py-2 text-sm text-emerald-800 dark:border-emerald-900/40 dark:bg-emerald-950/30 dark:text-emerald-200">
|
||||||
{{ __('localization.review.ready_for_publication') }}
|
{{ __('localization.review.ready_for_publication') }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
|
@php
|
||||||
|
$mappingVersion = \App\Support\Governance\Controls\ComplianceEvidenceMappingV1::VERSION_KEY;
|
||||||
|
@endphp
|
||||||
|
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||||
@ -12,6 +16,14 @@
|
|||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
{{ __('localization.review.customer_workspace_canonical_note') }}
|
{{ __('localization.review.customer_workspace_canonical_note') }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
|
{{ __('localization.review.customer_workspace_mapping_version', ['version' => $mappingVersion]) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rounded-md border border-amber-100 bg-amber-50 px-3 py-2 text-sm text-amber-800 dark:border-amber-900/40 dark:bg-amber-950/30 dark:text-amber-200">
|
||||||
|
{{ __('localization.review.customer_workspace_non_certification_disclosure') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
|
|||||||
@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
Storage::disk('exports')->put('review-packs/customer-review-workspace-smoke.zip', 'PK-test');
|
Storage::disk('exports')->put('review-packs/customer-review-workspace-smoke.zip', 'PK-test');
|
||||||
|
|
||||||
ReviewPack::factory()->ready()->create([
|
$pack = ReviewPack::factory()->ready()->create([
|
||||||
'tenant_id' => (int) $tenantPublished->getKey(),
|
'tenant_id' => (int) $tenantPublished->getKey(),
|
||||||
'workspace_id' => (int) $tenantPublished->workspace_id,
|
'workspace_id' => (int) $tenantPublished->workspace_id,
|
||||||
'tenant_review_id' => (int) $publishedReview->getKey(),
|
'tenant_review_id' => (int) $publishedReview->getKey(),
|
||||||
@ -67,6 +67,8 @@
|
|||||||
'file_disk' => 'exports',
|
'file_disk' => 'exports',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$publishedReview->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||||
|
|
||||||
$this->actingAs($user)->withSession([
|
$this->actingAs($user)->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantPublished->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantPublished->workspace_id,
|
||||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => [
|
||||||
@ -83,13 +85,23 @@
|
|||||||
->waitForText('Customer-safe review workspace')
|
->waitForText('Customer-safe review workspace')
|
||||||
->assertSee('Clear filters')
|
->assertSee('Clear filters')
|
||||||
->assertSee('Open latest review')
|
->assertSee('Open latest review')
|
||||||
|
->assertSee('Control readiness')
|
||||||
|
->assertSee('Endpoint hardening and compliance')
|
||||||
|
->assertSee('Compliance evidence mapping v1')
|
||||||
|
->assertSee('This is not a certification, legal attestation, or compliance guarantee.')
|
||||||
|
->assertSee('Follow-up required')
|
||||||
->assertDontSee('Publish review')
|
->assertDontSee('Publish review')
|
||||||
->assertDontSee('Refresh review')
|
->assertDontSee('Refresh review')
|
||||||
->click('Clear filters')
|
->click('Clear filters')
|
||||||
->waitForText('No published review available yet')
|
->waitForText('Published Tenant')
|
||||||
->assertSee('No published review available yet')
|
->assertDontSee('No Published Tenant')
|
||||||
|
->assertDontSee('No published review available yet')
|
||||||
->click('Open latest review')
|
->click('Open latest review')
|
||||||
->waitForText('Outcome summary')
|
->waitForText('Outcome summary')
|
||||||
|
->assertSee('Download current review pack')
|
||||||
|
->assertSee('Released governance record')
|
||||||
|
->assertSee('Control readiness interpretation')
|
||||||
|
->assertSee('Compliance evidence mapping v1')
|
||||||
->assertDontSee('Publish review')
|
->assertDontSee('Publish review')
|
||||||
->assertDontSee('Refresh review')
|
->assertDontSee('Refresh review')
|
||||||
->assertDontSee('Create next review')
|
->assertDontSee('Create next review')
|
||||||
|
|||||||
@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\EvidenceSnapshot;
|
||||||
use App\Support\Audit\AuditActionId;
|
use App\Support\Audit\AuditActionId;
|
||||||
use App\Support\Evidence\EvidenceCompletenessState;
|
use App\Support\Evidence\EvidenceCompletenessState;
|
||||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||||
@ -31,3 +34,38 @@
|
|||||||
->and(AuditLog::query()->where('action', AuditActionId::EvidenceSnapshotExpired->value)->exists())->toBeTrue()
|
->and(AuditLog::query()->where('action', AuditActionId::EvidenceSnapshotExpired->value)->exists())->toBeTrue()
|
||||||
->and(data_get($expiredAudit?->metadata, 'reason'))->toBe('Evidence basis is obsolete.');
|
->and(data_get($expiredAudit?->metadata, 'reason'))->toBe('Evidence basis is obsolete.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('records audit entries when customer review proof is opened explicitly', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
|
||||||
|
$snapshot = EvidenceSnapshot::query()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'status' => EvidenceSnapshotStatus::Active->value,
|
||||||
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||||
|
'summary' => ['finding_count' => 1],
|
||||||
|
'generated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant').'?'.http_build_query([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => '123',
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => 'compliance_evidence_mapping.v1',
|
||||||
|
]))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$audit = AuditLog::query()
|
||||||
|
->where('action', AuditActionId::EvidenceSnapshotOpened->value)
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
expect($audit)->not->toBeNull()
|
||||||
|
->and($audit?->resource_type)->toBe('evidence_snapshot')
|
||||||
|
->and(data_get($audit?->metadata, 'evidence_snapshot_id'))->toBe((int) $snapshot->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe(CustomerReviewWorkspace::SOURCE_SURFACE)
|
||||||
|
->and(data_get($audit?->metadata, 'review_id'))->toBe('123')
|
||||||
|
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe('compliance_evidence_mapping.v1');
|
||||||
|
});
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||||
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots;
|
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ListEvidenceSnapshots;
|
||||||
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ViewEvidenceSnapshot;
|
use App\Filament\Resources\EvidenceSnapshotResource\Pages\ViewEvidenceSnapshot;
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Jobs\GenerateEvidenceSnapshotJob;
|
use App\Jobs\GenerateEvidenceSnapshotJob;
|
||||||
use App\Models\EvidenceSnapshot;
|
use App\Models\EvidenceSnapshot;
|
||||||
use App\Models\EvidenceSnapshotItem;
|
use App\Models\EvidenceSnapshotItem;
|
||||||
@ -419,6 +420,63 @@ function suspendEvidenceSnapshotWorkspace(Tenant $tenant): void
|
|||||||
->assertSeeText('Copy JSON');
|
->assertSeeText('Copy JSON');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides evidence refresh, expiry, operation, fingerprint, and raw json in the customer review proof flow', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
$run = OperationRun::factory()->forTenant($tenant)->create();
|
||||||
|
|
||||||
|
$snapshot = EvidenceSnapshot::query()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'operation_run_id' => (int) $run->getKey(),
|
||||||
|
'status' => EvidenceSnapshotStatus::Active->value,
|
||||||
|
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||||
|
'summary' => ['finding_count' => 1],
|
||||||
|
'fingerprint' => hash('sha256', 'customer-proof-flow'),
|
||||||
|
'generated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
EvidenceSnapshotItem::query()->create([
|
||||||
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'dimension_key' => 'findings_summary',
|
||||||
|
'state' => EvidenceCompletenessState::Complete->value,
|
||||||
|
'required' => true,
|
||||||
|
'source_kind' => 'model_summary',
|
||||||
|
'summary_payload' => ['count' => 1, 'open_count' => 0],
|
||||||
|
'sort_order' => 10,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $tenant, panel: 'tenant').'?'.http_build_query([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => '456',
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => 'compliance_evidence_mapping.v1',
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Evidence dimensions')
|
||||||
|
->assertDontSee('Open the latest evidence refresh operation.')
|
||||||
|
->assertDontSee('customer-proof-flow')
|
||||||
|
->assertDontSee('Raw summary JSON')
|
||||||
|
->assertDontSee('Copy JSON');
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
Livewire::withQueryParams([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'review_id' => '456',
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => 'compliance_evidence_mapping.v1',
|
||||||
|
])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(ViewEvidenceSnapshot::class, ['record' => $snapshot->getKey()])
|
||||||
|
->assertActionDoesNotExist('refresh_evidence')
|
||||||
|
->assertActionDoesNotExist('expire_snapshot');
|
||||||
|
});
|
||||||
|
|
||||||
it('hides expire actions for expired snapshots on list and detail surfaces', function (): void {
|
it('hides expire actions for expired snapshots on list and detail surfaces', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use App\Models\ReviewPack;
|
use App\Models\ReviewPack;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
|
use App\Models\OperationRun;
|
||||||
use App\Models\PlatformUser;
|
use App\Models\PlatformUser;
|
||||||
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
|
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
|
||||||
use App\Services\ReviewPackService;
|
use App\Services\ReviewPackService;
|
||||||
@ -66,7 +67,12 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
|||||||
|
|
||||||
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||||
'source_surface' => 'customer_review_workspace',
|
'source_surface' => 'customer_review_workspace',
|
||||||
|
'review_id' => '789',
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => 'compliance_evidence_mapping.v1',
|
||||||
]);
|
]);
|
||||||
|
$packCount = ReviewPack::query()->count();
|
||||||
|
$operationRunCount = OperationRun::query()->count();
|
||||||
|
|
||||||
$response = $this->actingAs($user)->get($signedUrl);
|
$response = $this->actingAs($user)->get($signedUrl);
|
||||||
|
|
||||||
@ -82,7 +88,12 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
|||||||
expect($audit)->not->toBeNull()
|
expect($audit)->not->toBeNull()
|
||||||
->and($audit?->resource_type)->toBe('review_pack')
|
->and($audit?->resource_type)->toBe('review_pack')
|
||||||
->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey())
|
->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey())
|
||||||
->and(data_get($audit?->metadata, 'source_surface'))->toBe('customer_review_workspace');
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe('customer_review_workspace')
|
||||||
|
->and(data_get($audit?->metadata, 'review_id'))->toBe('789')
|
||||||
|
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe('compliance_evidence_mapping.v1')
|
||||||
|
->and(ReviewPack::query()->count())->toBe($packCount)
|
||||||
|
->and(OperationRun::query()->count())->toBe($operationRunCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps ready pack downloads available while the workspace is suspended read-only', function (): void {
|
it('keeps ready pack downloads available while the workspace is suspended read-only', function (): void {
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Resources\ReviewPackResource;
|
use App\Filament\Resources\ReviewPackResource;
|
||||||
use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks;
|
use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks;
|
||||||
use App\Filament\Resources\ReviewPackResource\Pages\ViewReviewPack;
|
use App\Filament\Resources\ReviewPackResource\Pages\ViewReviewPack;
|
||||||
@ -644,6 +645,40 @@ function seedReviewPackEvidence(Tenant $tenant): EvidenceSnapshot
|
|||||||
->assertActionVisible('regenerate');
|
->assertActionVisible('regenerate');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('hides regenerate and raw pack diagnostics in the customer review pack flow', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
|
||||||
|
$pack = ReviewPack::factory()->ready()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'initiated_by_user_id' => (int) $user->getKey(),
|
||||||
|
'sha256' => hash('sha256', 'customer-pack-flow'),
|
||||||
|
'fingerprint' => hash('sha256', 'customer-pack-fingerprint'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $tenant, panel: 'tenant').'?'.http_build_query([
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Outcome summary')
|
||||||
|
->assertDontSee('Regenerate')
|
||||||
|
->assertDontSee('SHA-256')
|
||||||
|
->assertDontSee('Fingerprint')
|
||||||
|
->assertDontSee('Include PII')
|
||||||
|
->assertDontSee('Include operations');
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
Filament::setTenant($tenant, true);
|
||||||
|
|
||||||
|
Livewire::withQueryParams(['source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
|
||||||
|
->assertActionVisible('download')
|
||||||
|
->assertActionDoesNotExist('regenerate');
|
||||||
|
});
|
||||||
|
|
||||||
// ─── Non-Member Access ───────────────────────────────────────
|
// ─── Non-Member Access ───────────────────────────────────────
|
||||||
|
|
||||||
it('returns 404 for non-members on list page', function (): void {
|
it('returns 404 for non-members on list page', function (): void {
|
||||||
|
|||||||
@ -3,10 +3,12 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
|
use App\Models\AuditLog;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\Workspace;
|
use App\Models\Workspace;
|
||||||
use App\Models\WorkspaceMembership;
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Support\Audit\AuditActionId;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
@ -46,6 +48,16 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get(CustomerReviewWorkspace::getUrl(panel: 'admin'))
|
->get(CustomerReviewWorkspace::getUrl(panel: 'admin'))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
|
$audit = AuditLog::query()
|
||||||
|
->where('action', AuditActionId::CustomerReviewWorkspaceOpened->value)
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
expect($audit)->not->toBeNull()
|
||||||
|
->and($audit?->resource_type)->toBe('customer_review_workspace')
|
||||||
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe(CustomerReviewWorkspace::SOURCE_SURFACE)
|
||||||
|
->and(data_get($audit?->metadata, 'entitled_tenant_count'))->toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns 404 for explicit out-of-scope tenant targeting on the customer review workspace', function (): void {
|
it('returns 404 for explicit out-of-scope tenant targeting on the customer review workspace', function (): void {
|
||||||
|
|||||||
@ -136,17 +136,37 @@
|
|||||||
'published_by_user_id' => (int) $user->getKey(),
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
|
Storage::disk('exports')->put('review-packs/customer-workspace-detail-download.zip', 'PK-test');
|
||||||
|
|
||||||
|
$pack = ReviewPack::factory()->ready()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_review_id' => (int) $review->getKey(),
|
||||||
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
|
'initiated_by_user_id' => (int) $user->getKey(),
|
||||||
|
'file_path' => 'review-packs/customer-workspace-detail-download.zip',
|
||||||
|
'file_disk' => 'exports',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||||
|
|
||||||
setTenantPanelContext($tenant);
|
setTenantPanelContext($tenant);
|
||||||
|
|
||||||
Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1])
|
Livewire::withQueryParams([
|
||||||
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||||
|
])
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
||||||
->assertSee('Outcome summary')
|
->assertSee('Outcome summary')
|
||||||
|
->assertActionVisible('download_current_review_pack')
|
||||||
->assertActionDoesNotExist('publish_review')
|
->assertActionDoesNotExist('publish_review')
|
||||||
->assertActionDoesNotExist('refresh_review')
|
->assertActionDoesNotExist('refresh_review')
|
||||||
->assertActionDoesNotExist('create_next_review')
|
->assertActionDoesNotExist('create_next_review')
|
||||||
->assertActionDoesNotExist('export_executive_pack')
|
->assertActionDoesNotExist('export_executive_pack')
|
||||||
->assertActionHidden('archive_review');
|
->assertActionDoesNotExist('archive_review');
|
||||||
|
|
||||||
$audit = AuditLog::query()
|
$audit = AuditLog::query()
|
||||||
->where('action', AuditActionId::TenantReviewOpened->value)
|
->where('action', AuditActionId::TenantReviewOpened->value)
|
||||||
@ -156,5 +176,7 @@
|
|||||||
expect($audit)->not->toBeNull()
|
expect($audit)->not->toBeNull()
|
||||||
->and($audit?->resource_type)->toBe('tenant_review')
|
->and($audit?->resource_type)->toBe('tenant_review')
|
||||||
->and(data_get($audit?->metadata, 'review_id'))->toBe((int) $review->getKey())
|
->and(data_get($audit?->metadata, 'review_id'))->toBe((int) $review->getKey())
|
||||||
->and(data_get($audit?->metadata, 'source_surface'))->toBe('customer_review_workspace');
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe(CustomerReviewWorkspace::SOURCE_SURFACE)
|
||||||
|
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe($review->controlInterpretationVersion());
|
||||||
});
|
});
|
||||||
@ -6,12 +6,15 @@
|
|||||||
use App\Models\PlatformUser;
|
use App\Models\PlatformUser;
|
||||||
use App\Models\ReviewPack;
|
use App\Models\ReviewPack;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
|
use App\Services\Entitlements\WorkspaceCommercialLifecycleResolver;
|
||||||
use App\Services\Settings\SettingsWriter;
|
use App\Services\Settings\SettingsWriter;
|
||||||
|
use App\Support\Auth\Capabilities;
|
||||||
use App\Support\Auth\PlatformCapabilities;
|
use App\Support\Auth\PlatformCapabilities;
|
||||||
use App\Support\TenantReviewStatus;
|
use App\Support\TenantReviewStatus;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
@ -33,7 +36,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('shows the ready review-pack action for the latest published review', function (): void {
|
it('keeps the latest released review as the only row action when a ready review pack exists', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
$snapshot = seedTenantReviewEvidence($tenant);
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
@ -65,11 +68,11 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(CustomerReviewWorkspace::class)
|
->test(CustomerReviewWorkspace::class)
|
||||||
->assertTableActionVisible('open_latest_review', $tenant)
|
->assertTableActionVisible('open_latest_review', $tenant)
|
||||||
->assertTableActionVisible('download_review_pack', $tenant)
|
->assertDontSee('Download review pack')
|
||||||
->assertSee('Available');
|
->assertDontSee('Current review pack available');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps customer review workspace and pack actions visible while suspended read-only', function (): void {
|
it('keeps the customer review workspace row action visible while suspended read-only', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
$snapshot = seedTenantReviewEvidence($tenant);
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
@ -103,11 +106,11 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(CustomerReviewWorkspace::class)
|
->test(CustomerReviewWorkspace::class)
|
||||||
->assertTableActionVisible('open_latest_review', $tenant)
|
->assertTableActionVisible('open_latest_review', $tenant)
|
||||||
->assertTableActionVisible('download_review_pack', $tenant)
|
->assertDontSee('Download review pack')
|
||||||
->assertSee('Available');
|
->assertDontSee('Current review pack available');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows an unavailable pack state and hides the download action when no current review pack exists', function (): void {
|
it('does not expose review-pack availability as a workspace row peer action', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
$snapshot = seedTenantReviewEvidence($tenant);
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
@ -127,11 +130,55 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(CustomerReviewWorkspace::class)
|
->test(CustomerReviewWorkspace::class)
|
||||||
->assertTableActionVisible('open_latest_review', $tenant)
|
->assertTableActionVisible('open_latest_review', $tenant)
|
||||||
->assertTableActionHidden('download_review_pack', $tenant)
|
->assertDontSee('No current review pack available yet')
|
||||||
->assertSee('Unavailable');
|
->assertDontSee('Download review pack');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('hides review and pack actions for tenants without a published review', function (): void {
|
it('keeps expired and capability-blocked review-pack states off the workspace row surface', function (): void {
|
||||||
|
$expiredTenant = Tenant::factory()->create(['name' => 'Expired Pack Tenant']);
|
||||||
|
[$user, $expiredTenant] = createUserWithTenant(tenant: $expiredTenant, role: 'readonly');
|
||||||
|
$blockedTenant = Tenant::factory()->create([
|
||||||
|
'workspace_id' => (int) $expiredTenant->workspace_id,
|
||||||
|
'name' => 'Blocked Pack Tenant',
|
||||||
|
]);
|
||||||
|
createUserWithTenant(tenant: $blockedTenant, user: $user, role: 'readonly');
|
||||||
|
|
||||||
|
foreach ([$expiredTenant, $blockedTenant] as $tenant) {
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
$review->forceFill([
|
||||||
|
'status' => TenantReviewStatus::Published->value,
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$pack = ReviewPack::factory()->ready()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_review_id' => (int) $review->getKey(),
|
||||||
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
|
'initiated_by_user_id' => (int) $user->getKey(),
|
||||||
|
'expires_at' => $tenant->is($expiredTenant) ? now()->subDay() : now()->addDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
Gate::define(Capabilities::REVIEW_PACK_VIEW, fn (User $actor, Tenant $tenant): bool => ! $tenant->is($blockedTenant));
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $expiredTenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(CustomerReviewWorkspace::class)
|
||||||
|
->assertCanSeeTableRecords([$expiredTenant->fresh(), $blockedTenant->fresh()])
|
||||||
|
->assertDontSee('Review pack expired')
|
||||||
|
->assertDontSee('Review pack access is unavailable for this actor')
|
||||||
|
->assertDontSee('Download review pack');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hides tenants without a published review from the workspace rows', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
$snapshot = seedTenantReviewEvidence($tenant);
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
@ -150,7 +197,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
|||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(CustomerReviewWorkspace::class)
|
->test(CustomerReviewWorkspace::class)
|
||||||
->assertTableActionHidden('open_latest_review', $tenant)
|
->assertCanNotSeeTableRecords([$tenant->fresh()])
|
||||||
->assertTableActionHidden('download_review_pack', $tenant)
|
->assertSee('No released customer reviews match this view')
|
||||||
->assertSee('No published review available yet');
|
->assertDontSee('No published review available yet');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -4,8 +4,11 @@
|
|||||||
|
|
||||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Models\FindingException;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
use App\Support\TenantReviewStatus;
|
use App\Support\TenantReviewStatus;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
@ -89,6 +92,10 @@
|
|||||||
->assertCanSeeTableRecords([$tenantA->fresh(), $tenantB->fresh()])
|
->assertCanSeeTableRecords([$tenantA->fresh(), $tenantB->fresh()])
|
||||||
->assertCanNotSeeTableRecords([$tenantDenied->fresh()])
|
->assertCanNotSeeTableRecords([$tenantDenied->fresh()])
|
||||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview->fresh()], $tenantA), false)
|
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview->fresh()], $tenantA), false)
|
||||||
|
->assertSee('Compliance evidence mapping v1')
|
||||||
|
->assertSee('This is not a certification, legal attestation, or compliance guarantee.')
|
||||||
|
->assertSee('Endpoint hardening and compliance')
|
||||||
|
->assertSee(ComplianceEvidenceMappingV1::VERSION_KEY)
|
||||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $betaPublishedReview->fresh()], $tenantB), false)
|
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $betaPublishedReview->fresh()], $tenantB), false)
|
||||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $olderPublishedReview->fresh()], $tenantA), false)
|
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $olderPublishedReview->fresh()], $tenantA), false)
|
||||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $newerInternalReview->fresh()], $tenantA), false)
|
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $newerInternalReview->fresh()], $tenantA), false)
|
||||||
@ -100,7 +107,7 @@
|
|||||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false);
|
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $deniedPublishedReview->fresh()], $tenantDenied), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows entitled tenants without a published review as calm absence rows', function (): void {
|
it('excludes entitled tenants without a published review from customer workspace rows', function (): void {
|
||||||
$tenantPublished = Tenant::factory()->create(['name' => 'Published Tenant']);
|
$tenantPublished = Tenant::factory()->create(['name' => 'Published Tenant']);
|
||||||
[$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly');
|
[$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly');
|
||||||
|
|
||||||
@ -133,13 +140,87 @@
|
|||||||
|
|
||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(CustomerReviewWorkspace::class)
|
->test(CustomerReviewWorkspace::class)
|
||||||
->assertCanSeeTableRecords([$tenantPublished->fresh(), $tenantWithoutPublished->fresh()])
|
->assertCanSeeTableRecords([$tenantPublished->fresh()])
|
||||||
->assertSee('No published review')
|
->assertCanNotSeeTableRecords([$tenantWithoutPublished->fresh()])
|
||||||
->assertSee('No published review available yet')
|
->assertDontSee('No published review')
|
||||||
|
->assertDontSee('No published review available yet')
|
||||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenantWithoutPublished), false)
|
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenantWithoutPublished), false)
|
||||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false);
|
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a page-level empty state when no entitled tenant has a released review', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create(['name' => 'Internal Only Tenant']);
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
|
|
||||||
|
$internalOnlyReview = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
$internalOnlyReview->forceFill([
|
||||||
|
'status' => TenantReviewStatus::Ready->value,
|
||||||
|
'published_at' => null,
|
||||||
|
'published_by_user_id' => null,
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(CustomerReviewWorkspace::class)
|
||||||
|
->assertCanNotSeeTableRecords([$tenant->fresh()])
|
||||||
|
->assertSee('No released customer reviews match this view')
|
||||||
|
->assertSee('Publish a tenant review before it appears in the customer-safe workspace.')
|
||||||
|
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenant), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('summarizes accepted-risk control interpretation and evidence proof availability in customer-safe workspace rows', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create(['name' => 'Governed Tenant']);
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
$owner = User::factory()->create(['name' => 'Risk Owner']);
|
||||||
|
$finding = Finding::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||||
|
]);
|
||||||
|
|
||||||
|
FindingException::query()->create([
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'status' => FindingException::STATUS_ACTIVE,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
||||||
|
'requested_by_user_id' => (int) $user->getKey(),
|
||||||
|
'request_reason' => 'Vendor patch window accepted by the customer.',
|
||||||
|
'owner_user_id' => (int) $owner->getKey(),
|
||||||
|
'approved_by_user_id' => (int) $owner->getKey(),
|
||||||
|
'requested_at' => now()->subDays(2),
|
||||||
|
'approved_at' => now()->subDay(),
|
||||||
|
'effective_from' => now()->subDay(),
|
||||||
|
'review_due_at' => now()->addDays(14),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||||
|
|
||||||
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
$review->forceFill([
|
||||||
|
'status' => TenantReviewStatus::Published->value,
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
setAdminPanelContext();
|
||||||
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||||
|
|
||||||
|
Livewire::actingAs($user)
|
||||||
|
->test(CustomerReviewWorkspace::class)
|
||||||
|
->assertCanSeeTableRecords([$tenant->fresh()])
|
||||||
|
->assertSee('Review recommended')
|
||||||
|
->assertSee('1 evidence signal(s) reference this control.')
|
||||||
|
->assertSee('1 accepted-risk finding(s) qualify this view.')
|
||||||
|
->assertSee('Review the accepted-risk owner and next review date before customer delivery.')
|
||||||
|
->assertSee('Accepted risk influences this view');
|
||||||
|
});
|
||||||
|
|
||||||
it('defaults the customer review workspace to the remembered tenant when tenant context is available', function (): void {
|
it('defaults the customer review workspace to the remembered tenant when tenant context is available', function (): void {
|
||||||
$tenantA = Tenant::factory()->create(['name' => 'Alpha Tenant']);
|
$tenantA = Tenant::factory()->create(['name' => 'Alpha Tenant']);
|
||||||
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly');
|
[$user, $tenantA] = createUserWithTenant(tenant: $tenantA, role: 'readonly');
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Jobs\GenerateReviewPackJob;
|
use App\Jobs\GenerateReviewPackJob;
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
use App\Models\EvidenceSnapshot;
|
use App\Models\EvidenceSnapshot;
|
||||||
use App\Services\ReviewPackService;
|
use App\Services\ReviewPackService;
|
||||||
@ -100,3 +102,38 @@
|
|||||||
->and(data_get($publishAudit?->metadata, 'reason'))->toBe('Publishing the current review pack.')
|
->and(data_get($publishAudit?->metadata, 'reason'))->toBe('Publishing the current review pack.')
|
||||||
->and(data_get($archiveAudit?->metadata, 'reason'))->toBe('Replacing with a newer governance review.');
|
->and(data_get($archiveAudit?->metadata, 'reason'))->toBe('Replacing with a newer governance review.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('records customer workspace interpretation metadata when a tenant review is opened', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
|
||||||
|
$review->forceFill([
|
||||||
|
'status' => 'published',
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
setTenantPanelContext($tenant);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([
|
||||||
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||||
|
]))
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$audit = AuditLog::query()
|
||||||
|
->where('action', AuditActionId::TenantReviewOpened->value)
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
expect($audit)->not->toBeNull()
|
||||||
|
->and($audit?->resource_type)->toBe('tenant_review')
|
||||||
|
->and(data_get($audit?->metadata, 'review_id'))->toBe((int) $review->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'source_surface'))->toBe(CustomerReviewWorkspace::SOURCE_SURFACE)
|
||||||
|
->and(data_get($audit?->metadata, 'tenant_filter_id'))->toBe((string) $tenant->getKey())
|
||||||
|
->and(data_get($audit?->metadata, 'interpretation_version'))->toBe($review->controlInterpretationVersion());
|
||||||
|
});
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Models\Finding;
|
use App\Models\Finding;
|
||||||
|
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||||
|
|
||||||
it('passes shared canonical control references through tenant review composition', function (): void {
|
it('passes shared canonical control references through tenant review composition', function (): void {
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
@ -11,12 +12,23 @@
|
|||||||
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
$openRisks = $review->sections->firstWhere('section_key', 'open_risks');
|
$openRisks = $review->sections->firstWhere('section_key', 'open_risks');
|
||||||
$executiveSummary = $review->sections->firstWhere('section_key', 'executive_summary');
|
$executiveSummary = $review->sections->firstWhere('section_key', 'executive_summary');
|
||||||
|
$controlInterpretation = $review->sections->firstWhere('section_key', ComplianceEvidenceMappingV1::SECTION_KEY);
|
||||||
|
$controlEntries = $review->controlInterpretationControls();
|
||||||
|
|
||||||
expect($review->canonicalControlReferences())->toHaveCount(1)
|
expect($review->canonicalControlReferences())->toHaveCount(1)
|
||||||
->and($review->canonicalControlReferences()[0]['control_key'])->toBe('endpoint_hardening_compliance')
|
->and($review->canonicalControlReferences()[0]['control_key'])->toBe('endpoint_hardening_compliance')
|
||||||
->and($executiveSummary->summary_payload['canonical_control_count'])->toBe(1)
|
->and($executiveSummary->summary_payload['canonical_control_count'])->toBe(1)
|
||||||
->and($executiveSummary->summary_payload['canonical_controls'][0]['control_key'])->toBe('endpoint_hardening_compliance')
|
->and($executiveSummary->summary_payload['canonical_controls'][0]['control_key'])->toBe('endpoint_hardening_compliance')
|
||||||
->and($openRisks->summary_payload['canonical_controls'][0]['control_key'] ?? null)->toBe('endpoint_hardening_compliance');
|
->and($openRisks->summary_payload['canonical_controls'][0]['control_key'] ?? null)->toBe('endpoint_hardening_compliance')
|
||||||
|
->and($review->controlInterpretationVersion())->toBe(ComplianceEvidenceMappingV1::VERSION_KEY)
|
||||||
|
->and($review->controlInterpretation()['non_certification_disclosure'] ?? null)->toBeString()
|
||||||
|
->and($review->controlInterpretation()['mapped_control_count'] ?? null)->toBe(1)
|
||||||
|
->and($controlEntries)->toHaveCount(1)
|
||||||
|
->and($controlEntries[0]['control_key'] ?? null)->toBe('endpoint_hardening_compliance')
|
||||||
|
->and($controlEntries[0]['readiness_bucket'] ?? null)->toBe('follow_up_required')
|
||||||
|
->and($controlEntries[0]['proof_access_state'] ?? null)->toBe('available')
|
||||||
|
->and($controlInterpretation?->summary_payload['version_key'] ?? null)->toBe(ComplianceEvidenceMappingV1::VERSION_KEY)
|
||||||
|
->and($controlInterpretation?->render_payload['entries'][0]['control_key'] ?? null)->toBe('endpoint_hardening_compliance');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('excludes removed acknowledged findings from open risk highlights', function (): void {
|
it('excludes removed acknowledged findings from open risk highlights', function (): void {
|
||||||
|
|||||||
@ -3,8 +3,10 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
|
use App\Support\OperationRunLinks;
|
||||||
use App\Support\ReasonTranslation\ReasonPresenter;
|
use App\Support\ReasonTranslation\ReasonPresenter;
|
||||||
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
||||||
use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext;
|
use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext;
|
||||||
@ -62,3 +64,46 @@
|
|||||||
->assertSee($registerOutcome?->primaryReason ?? '')
|
->assertSee($registerOutcome?->primaryReason ?? '')
|
||||||
->assertSee($explanation?->nextActionText ?? '');
|
->assertSee($explanation?->nextActionText ?? '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps customer-workspace review detail customer-readable by hiding internal reason ownership and fingerprints', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
$review->forceFill([
|
||||||
|
'status' => 'published',
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
expect($review->operation_run_id)->not->toBeNull();
|
||||||
|
|
||||||
|
setTenantPanelContext($tenant);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([
|
||||||
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
||||||
|
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||||
|
'tenant_filter_id' => (string) $tenant->getKey(),
|
||||||
|
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Released governance record')
|
||||||
|
->assertSee('This released review is available for customer-safe governance consumption.')
|
||||||
|
->assertSee('Compliance evidence mapping v1')
|
||||||
|
->assertSee($review->controlInterpretationVersion())
|
||||||
|
->assertSee('Control readiness interpretation')
|
||||||
|
->assertSee('Endpoint hardening and compliance')
|
||||||
|
->assertSee('Evidence basis')
|
||||||
|
->assertSee('Review the surfaced findings with the tenant and agree ownership plus follow-up timing.')
|
||||||
|
->assertSee('Evidence snapshot')
|
||||||
|
->assertSee('review_id='.$review->getKey(), false)
|
||||||
|
->assertSee('interpretation_version='.$review->controlInterpretationVersion(), false)
|
||||||
|
->assertSee('source_surface=customer_review_workspace', false)
|
||||||
|
->assertDontSee('Reason owner')
|
||||||
|
->assertDontSee('Platform reason family')
|
||||||
|
->assertDontSee('Fingerprint')
|
||||||
|
->assertDontSee(OperationRunLinks::tenantlessView((int) $review->operation_run_id), false)
|
||||||
|
->assertDontSee('Inspect the latest review composition or refresh run.');
|
||||||
|
});
|
||||||
|
|||||||
@ -3,23 +3,31 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Filament\Pages\Reviews\ReviewRegister;
|
use App\Filament\Pages\Reviews\ReviewRegister;
|
||||||
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
use App\Filament\Resources\TenantReviewResource\Pages\ListTenantReviews;
|
use App\Filament\Resources\TenantReviewResource\Pages\ListTenantReviews;
|
||||||
use App\Filament\Resources\TenantReviewResource\Pages\ViewTenantReview;
|
use App\Filament\Resources\TenantReviewResource\Pages\ViewTenantReview;
|
||||||
|
use App\Models\ReviewPack;
|
||||||
use App\Models\Tenant;
|
use App\Models\Tenant;
|
||||||
use App\Models\TenantReview;
|
use App\Models\TenantReview;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Support\TenantReviewStatus;
|
||||||
use App\Services\TenantReviews\TenantReviewLifecycleService;
|
use App\Services\TenantReviews\TenantReviewLifecycleService;
|
||||||
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
use App\Support\Ui\GovernanceActions\GovernanceActionCatalog;
|
||||||
use App\Support\Workspaces\WorkspaceContext;
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
use Filament\Actions\Action;
|
use Filament\Actions\Action;
|
||||||
use Filament\Actions\ActionGroup;
|
use Filament\Actions\ActionGroup;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
use Livewire\Features\SupportTesting\Testable;
|
use Livewire\Features\SupportTesting\Testable;
|
||||||
use Livewire\Livewire;
|
use Livewire\Livewire;
|
||||||
use Tests\Feature\Concerns\BuildsGovernanceArtifactTruthFixtures;
|
use Tests\Feature\Concerns\BuildsGovernanceArtifactTruthFixtures;
|
||||||
|
|
||||||
uses(BuildsGovernanceArtifactTruthFixtures::class);
|
uses(BuildsGovernanceArtifactTruthFixtures::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
Storage::fake('exports');
|
||||||
|
});
|
||||||
|
|
||||||
function tenantReviewContractHeaderActions(Testable $component): array
|
function tenantReviewContractHeaderActions(Testable $component): array
|
||||||
{
|
{
|
||||||
$instance = $component->instance();
|
$instance = $component->instance();
|
||||||
@ -161,6 +169,54 @@ function tenantReviewContractHeaderActions(Testable $component): array
|
|||||||
->and($groupLabels)->toBe(['More', 'Danger']);
|
->and($groupLabels)->toBe(['More', 'Danger']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses the current review-pack download as the only customer-workspace detail header action', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||||
|
$snapshot = seedTenantReviewEvidence($tenant);
|
||||||
|
|
||||||
|
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||||
|
$review->forceFill([
|
||||||
|
'status' => TenantReviewStatus::Published->value,
|
||||||
|
'published_at' => now(),
|
||||||
|
'published_by_user_id' => (int) $user->getKey(),
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
Storage::disk('exports')->put('review-packs/customer-detail-primary.zip', 'PK-test');
|
||||||
|
|
||||||
|
$pack = ReviewPack::factory()->ready()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'tenant_review_id' => (int) $review->getKey(),
|
||||||
|
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||||
|
'initiated_by_user_id' => (int) $user->getKey(),
|
||||||
|
'file_path' => 'review-packs/customer-detail-primary.zip',
|
||||||
|
'file_disk' => 'exports',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save();
|
||||||
|
|
||||||
|
setTenantPanelContext($tenant);
|
||||||
|
|
||||||
|
$component = Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1])
|
||||||
|
->actingAs($user)
|
||||||
|
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
||||||
|
->assertActionVisible('download_current_review_pack')
|
||||||
|
->assertActionEnabled('download_current_review_pack')
|
||||||
|
->assertActionDoesNotExist('publish_review')
|
||||||
|
->assertActionDoesNotExist('refresh_review')
|
||||||
|
->assertActionDoesNotExist('create_next_review')
|
||||||
|
->assertActionDoesNotExist('export_executive_pack')
|
||||||
|
->assertActionDoesNotExist('archive_review');
|
||||||
|
|
||||||
|
$topLevelActionNames = collect(tenantReviewContractHeaderActions($component))
|
||||||
|
->map(static fn ($action): ?string => $action instanceof Action ? $action->getName() : null)
|
||||||
|
->filter()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
|
||||||
|
expect($topLevelActionNames)->toBe(['download_current_review_pack']);
|
||||||
|
});
|
||||||
|
|
||||||
it('shows publication truth and next-step guidance when a review is not yet publishable', function (): void {
|
it('shows publication truth and next-step guidance when a review is not yet publishable', function (): void {
|
||||||
$tenant = Tenant::factory()->create();
|
$tenant = Tenant::factory()->create();
|
||||||
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
[$owner, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|||||||
@ -17,12 +17,15 @@
|
|||||||
|
|
||||||
expect(array_column($payload['sections'], 'section_key'))->toBe([
|
expect(array_column($payload['sections'], 'section_key'))->toBe([
|
||||||
'executive_summary',
|
'executive_summary',
|
||||||
|
'control_interpretation',
|
||||||
'open_risks',
|
'open_risks',
|
||||||
'accepted_risks',
|
'accepted_risks',
|
||||||
'permission_posture',
|
'permission_posture',
|
||||||
'baseline_drift_posture',
|
'baseline_drift_posture',
|
||||||
'operations_health',
|
'operations_health',
|
||||||
])->and($payload['status'])->toBe(TenantReviewStatus::Ready->value);
|
])->and($payload['summary']['section_count'])->toBe(7)
|
||||||
|
->and($payload['summary']['control_interpretation']['version_key'] ?? null)->toBe('compliance_evidence_mapping.v1')
|
||||||
|
->and($payload['status'])->toBe(TenantReviewStatus::Ready->value);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('marks reviews as ready when evidence is partial but required sections are still present', function (): void {
|
it('marks reviews as ready when evidence is partial but required sections are still present', function (): void {
|
||||||
|
|||||||
@ -0,0 +1,54 @@
|
|||||||
|
# Preparation Review Checklist: Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Purpose**: Validate repo-fit preparation quality after `spec.md`, `plan.md`, and `tasks.md` are complete
|
||||||
|
**Reviewed**: 2026-04-30
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
**Supporting artifacts**: [plan.md](../plan.md), [tasks.md](../tasks.md), [research.md](../research.md), [data-model.md](../data-model.md), [quickstart.md](../quickstart.md), [customer-review-productization.openapi.yaml](../contracts/customer-review-productization.openapi.yaml)
|
||||||
|
|
||||||
|
## Candidate Fit
|
||||||
|
|
||||||
|
- [x] The selected candidate still matches the active P0 queue in `docs/product/spec-candidates.md`, the current priority order in `docs/product/roadmap.md`, and the open-gap wording in `docs/product/implementation-ledger.md`
|
||||||
|
- [x] Existing `specs/` coverage was checked so this package stays a new productization follow-up rather than a duplicate of Specs 249, 253, 254, 255, or 257
|
||||||
|
- [x] The scope stays on the customer-review productization delta over the existing workspace and released-review detail flow instead of reopening review foundations
|
||||||
|
- [x] Broader baseline/control overlays and management-packaging follow-through are explicitly deferred rather than hidden inside this slice
|
||||||
|
|
||||||
|
## Constitution Fit
|
||||||
|
|
||||||
|
- [x] The package stays on the existing Filament v5 + Livewire v4 admin plane and does not introduce panel/provider-registration work beyond the current `bootstrap/providers.php` truth
|
||||||
|
- [x] No new persistence, customer identity plane, portal shell, authoring flow, publication engine, remediation flow, or destructive action surface is introduced
|
||||||
|
- [x] Workspace/tenant isolation and capability-first RBAC remain explicit, including `404` for non-members and optional capability gating only for secondary access paths
|
||||||
|
- [x] One dominant safe action per changed surface is explicitly described, with secondary proof affordances demoted out of peer header-action status
|
||||||
|
- [x] Global-search safety is preserved without introducing a new searchable resource or widening existing review/evidence discovery across tenant boundaries
|
||||||
|
- [x] Asset strategy remains unchanged; if later implementation unexpectedly registers assets, deployment still uses the existing `cd apps/platform && php artisan filament:assets` step
|
||||||
|
|
||||||
|
## Artifact Consistency
|
||||||
|
|
||||||
|
- [x] `spec.md`, `plan.md`, and `tasks.md` all target the same workspace-summary plus released-review-detail follow-up
|
||||||
|
- [x] The likely repo surfaces and plan structure match the current repository layout, including `apps/platform/lang` rather than a fictional app-local language directory
|
||||||
|
- [x] Tasks directly cover RBAC, auditability, disclosure hierarchy, localization, access/unavailable states, and global-search safety
|
||||||
|
- [x] Supporting artifacts exist, no unresolved template markers remain, and the package stays implementation-ready without touching application code
|
||||||
|
|
||||||
|
## Test Governance
|
||||||
|
|
||||||
|
- [x] Validation lanes remain explicitly bounded to `confidence` plus one existing `browser` smoke
|
||||||
|
- [x] The package reuses the existing reviews test family instead of creating a new heavy-governance or browser family
|
||||||
|
- [x] Reviewer proof commands remain explicit and minimal for the touched workspace, detail, pack, and proof surfaces
|
||||||
|
- [x] The close-out path records the review outcome, guardrail status, and any `document-in-feature` vs `follow-up-spec` decision inside the spec package
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reviewed after artifact alignment on 2026-04-30.
|
||||||
|
- This repository's preparation artifacts are intentionally implementation-oriented, so concrete routes, classes, affected surfaces, and validation commands are expected rather than treated as leakage.
|
||||||
|
- No application implementation was performed while preparing or reviewing this package.
|
||||||
|
- Implementation close-out on 2026-04-30 passed the focused feature checks, bounded browser smoke, and Pint. Audit gaps were handled with bounded additive action IDs for workspace entry and proof-open events; global-search and asset strategy remained unchanged.
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
- **Outcome**: `keep`
|
||||||
|
- **Reason**: The package remains the narrow customer-review productization follow-up, explicitly records the baseline/control deferral, aligns the detail-page action hierarchy, and adds direct task coverage for global-search safety.
|
||||||
|
- **Workflow result**: Ready for `/speckit.implement` after this preparation review.
|
||||||
|
|
||||||
|
## Implementation Outcome
|
||||||
|
|
||||||
|
- **Outcome**: `implemented`
|
||||||
|
- **Workflow result**: Ready for manual review after the implementation loop. No confirmed in-scope findings remain after the focused confidence checks, browser smoke, formatting, and post-implementation analysis.
|
||||||
@ -0,0 +1,299 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: TenantPilot Customer Review Workspace Productization v1 (Conceptual)
|
||||||
|
version: 0.1.0
|
||||||
|
description: |
|
||||||
|
Conceptual contract for the customer-safe productization follow-up in Spec 258.
|
||||||
|
|
||||||
|
NOTE: These paths describe existing admin and tenant-scoped routes reused by
|
||||||
|
the implementation. The schemas document expected derived page/view behavior
|
||||||
|
for planning purposes only; they do not require a new public REST API.
|
||||||
|
servers:
|
||||||
|
- url: /
|
||||||
|
paths:
|
||||||
|
/admin/reviews/workspace:
|
||||||
|
get:
|
||||||
|
summary: View the productized customer review workspace
|
||||||
|
description: |
|
||||||
|
Existing canonical admin-plane workspace page for customer-safe review
|
||||||
|
consumption. The route stays read-only and reuses current tenant review,
|
||||||
|
finding, evidence, review-pack, localization, RBAC, and audit truth.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: tenant
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Optional tenant prefilter using the existing tenant id or external id
|
||||||
|
pattern already accepted by the workspace page.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Workspace page rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewWorkspacePageModel'
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, actors without entitled tenants, or explicit out-of-scope tenant targeting
|
||||||
|
|
||||||
|
/admin/t/{tenant}/reviews/{review}:
|
||||||
|
get:
|
||||||
|
summary: Open the released review detail from the customer review workspace
|
||||||
|
description: |
|
||||||
|
Existing tenant-scoped released-review detail route reused as the
|
||||||
|
secondary context surface from the workspace page. The customer-workspace
|
||||||
|
flow uses the existing `customer_workspace=1` query flag to keep the
|
||||||
|
detail read-only and customer-safe.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tenant
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: path
|
||||||
|
name: review
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: customer_workspace
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
description: Existing query-context flag that suppresses operator lifecycle actions on the detail surface.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Released review detail rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewDetailModel'
|
||||||
|
'403':
|
||||||
|
description: Forbidden for an in-scope actor missing the record-level review permission
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, tenant mismatches, or out-of-scope review targets
|
||||||
|
|
||||||
|
/admin/t/{tenant}/evidence/{evidenceSnapshot}:
|
||||||
|
get:
|
||||||
|
summary: Open an evidence proof route from the customer review flow
|
||||||
|
description: |
|
||||||
|
Existing tenant-scoped evidence detail route reused only when the actor
|
||||||
|
explicitly asks for proof and has the required capability.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tenant
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: path
|
||||||
|
name: evidenceSnapshot
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: source_surface
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Optional source-surface metadata if proof access is audited through the shared audit pipeline.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Evidence proof detail rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
'403':
|
||||||
|
description: Forbidden for an in-scope actor missing evidence capability
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, mismatched tenant scope, or unavailable proof targets
|
||||||
|
|
||||||
|
/admin/review-packs/{reviewPack}/download:
|
||||||
|
get:
|
||||||
|
summary: Download the current review pack
|
||||||
|
description: |
|
||||||
|
Existing signed download route reused by the productized customer review
|
||||||
|
flow. The pack must already exist, be ready, and not be expired.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: reviewPack
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: source_surface
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Existing download metadata hook used by the shared audit path.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Review pack download stream
|
||||||
|
content:
|
||||||
|
application/zip:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: binary
|
||||||
|
'403':
|
||||||
|
description: Forbidden because of missing signature or invalid signed URL
|
||||||
|
'404':
|
||||||
|
description: Review pack not found, not ready, expired, or out of accessible tenant scope
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
CustomerReviewWorkspacePageModel:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- workspace_id
|
||||||
|
- entries
|
||||||
|
properties:
|
||||||
|
workspace_id:
|
||||||
|
type: integer
|
||||||
|
tenant_filter_id:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
entries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewWorkspaceEntry'
|
||||||
|
empty_state_message:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
audit_expectation:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: |
|
||||||
|
Planning-only note describing whether workspace-open auditing is
|
||||||
|
already covered or requires a bounded shared-audit extension.
|
||||||
|
|
||||||
|
CustomerReviewWorkspaceEntry:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- tenant_id
|
||||||
|
- tenant_name
|
||||||
|
- review_access
|
||||||
|
- review_pack_access
|
||||||
|
- evidence_proof_access
|
||||||
|
properties:
|
||||||
|
tenant_id:
|
||||||
|
type: integer
|
||||||
|
tenant_name:
|
||||||
|
type: string
|
||||||
|
latest_published_review_id:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
latest_review_published_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
outcome_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
findings_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
accepted_risk_accountability_summary:
|
||||||
|
$ref: '#/components/schemas/AcceptedRiskAccountabilitySummary'
|
||||||
|
review_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
review_pack_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
evidence_proof_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
redaction_note:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
absence_note:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
CustomerReviewDetailModel:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- review_id
|
||||||
|
- tenant_id
|
||||||
|
- launched_from_customer_workspace
|
||||||
|
- operator_actions_hidden
|
||||||
|
properties:
|
||||||
|
review_id:
|
||||||
|
type: integer
|
||||||
|
tenant_id:
|
||||||
|
type: integer
|
||||||
|
launched_from_customer_workspace:
|
||||||
|
type: boolean
|
||||||
|
operator_actions_hidden:
|
||||||
|
type: boolean
|
||||||
|
narrative_outcome_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
findings_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
accepted_risk_accountability_summary:
|
||||||
|
$ref: '#/components/schemas/AcceptedRiskAccountabilitySummary'
|
||||||
|
evidence_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
review_pack_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
evidence_proof_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
secondary_diagnostics_collapsed:
|
||||||
|
type: boolean
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
AcceptedRiskAccountabilitySummary:
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
properties:
|
||||||
|
summary_text:
|
||||||
|
type: string
|
||||||
|
accountable_party:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
decision_reason:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
review_due_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
expires_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
nullable: true
|
||||||
|
completeness_note:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
AccessState:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- state
|
||||||
|
properties:
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- available
|
||||||
|
- absent
|
||||||
|
- unavailable
|
||||||
|
- expired
|
||||||
|
- redacted
|
||||||
|
- partial
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
audit_action_id:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
description: Existing or bounded-additive shared audit action id for the explicit access moment.
|
||||||
273
specs/258-customer-review-productization/data-model.md
Normal file
273
specs/258-customer-review-productization/data-model.md
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
# Data Model — Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Spec**: [spec.md](spec.md)
|
||||||
|
|
||||||
|
No new persisted tables, projections, or customer-review entities are required for this follow-up. The feature reuses current tenant-owned review, finding-exception, evidence, review-pack, membership, and audit truth, then tightens the derived workspace and detail presentation contracts.
|
||||||
|
|
||||||
|
## Persisted Truth Reused
|
||||||
|
|
||||||
|
### Workspace / Tenant Entitlement Context
|
||||||
|
|
||||||
|
**Purpose**: Establish the active workspace boundary and the entitled tenant set before any workspace rows, proof links, or review detail routes are composed.
|
||||||
|
|
||||||
|
**Persisted carriers**:
|
||||||
|
- existing workspace membership records
|
||||||
|
- existing tenant membership pivot rows and role assignments
|
||||||
|
- existing capability registry and role-capability map
|
||||||
|
|
||||||
|
**Relevant fields / contracts**:
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- tenant membership role
|
||||||
|
- capability grants derived from [../../apps/platform/app/Support/Auth/Capabilities.php](../../apps/platform/app/Support/Auth/Capabilities.php)
|
||||||
|
- current workspace and remembered tenant context from the existing workspace context/session model
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- current actor must be a member of the current workspace or the route resolves as not found
|
||||||
|
- workspace rows and explicit tenant filters may only resolve for entitled tenants in that current workspace
|
||||||
|
- out-of-scope tenant targets remain `404` and must not leak draft/review existence
|
||||||
|
|
||||||
|
### TenantReview
|
||||||
|
|
||||||
|
**Purpose**: Canonical source for the released governance record, current outcome summary, findings summary, accepted-risk summary, proof pointers, and review-detail inspect target.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `tenant_reviews` rows via [../../apps/platform/app/Models/TenantReview.php](../../apps/platform/app/Models/TenantReview.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `status`
|
||||||
|
- `generated_at`
|
||||||
|
- `published_at`
|
||||||
|
- `summary`
|
||||||
|
- `evidence_snapshot_id`
|
||||||
|
- `current_export_review_pack_id`
|
||||||
|
- `published_by_user_id`
|
||||||
|
- `tenant`
|
||||||
|
- `evidenceSnapshot`
|
||||||
|
- `currentExportReviewPack`
|
||||||
|
- `sections`
|
||||||
|
|
||||||
|
**Embedded summary payload currently reused**:
|
||||||
|
- `finding_count`
|
||||||
|
- `finding_outcomes`
|
||||||
|
- `risk_acceptance.status_marked_count`
|
||||||
|
- `risk_acceptance.valid_governed_count`
|
||||||
|
- `risk_acceptance.warning_count`
|
||||||
|
- `publish_blockers`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- the workspace default path continues to use the latest published review per entitled tenant only
|
||||||
|
- internal-only review states remain off the customer-safe default path
|
||||||
|
- the customer-workspace drilldown stays on the existing review detail route under the existing query-context flag
|
||||||
|
- productization may refine how summary data is explained, but it must not move that truth into a new stored model
|
||||||
|
|
||||||
|
### FindingException
|
||||||
|
|
||||||
|
**Purpose**: Existing accepted-risk and accountability truth used to explain who accepted risk, why it is on record, and whether it needs follow-up.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `finding_exceptions` rows via [../../apps/platform/app/Models/FindingException.php](../../apps/platform/app/Models/FindingException.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `finding_id`
|
||||||
|
- `status`
|
||||||
|
- `current_validity_state`
|
||||||
|
- `requested_at`
|
||||||
|
- `approved_at`
|
||||||
|
- `effective_from`
|
||||||
|
- `expires_at`
|
||||||
|
- `review_due_at`
|
||||||
|
- `owner_user_id`
|
||||||
|
- `approved_by_user_id`
|
||||||
|
- `current_decision_id`
|
||||||
|
- `evidence_summary`
|
||||||
|
- `owner`
|
||||||
|
- `approver`
|
||||||
|
- `currentDecision`
|
||||||
|
- `evidenceReferences`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- accountability summaries should derive from existing owner/approver/current-decision truth where present
|
||||||
|
- missing accountable-person or accountable-role truth must surface as partial/unavailable disclosure, not fabricated customer-safe copy
|
||||||
|
- accepted-risk visibility remains read-only in this slice; no edit, renew, revoke, or approval behavior moves into the customer-safe path
|
||||||
|
|
||||||
|
### EvidenceSnapshot
|
||||||
|
|
||||||
|
**Purpose**: Existing proof artifact for evidence freshness, completeness, and optional supporting detail reached only after explicit user intent.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `evidence_snapshots` rows via [../../apps/platform/app/Models/EvidenceSnapshot.php](../../apps/platform/app/Models/EvidenceSnapshot.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `status`
|
||||||
|
- `completeness_state`
|
||||||
|
- `generated_at`
|
||||||
|
- `expires_at`
|
||||||
|
- `summary`
|
||||||
|
- `items`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- evidence proof remains optional, lower-priority, and capability-gated by the current evidence-view path
|
||||||
|
- raw payloads and unrestricted diagnostics remain out of the default-visible workspace and review detail path
|
||||||
|
- if implementation adds explicit proof-access auditing, it should stay on the shared audit pipeline
|
||||||
|
|
||||||
|
### ReviewPack
|
||||||
|
|
||||||
|
**Purpose**: Existing packaged governance artifact for current downloadable review output.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `review_packs` rows via [../../apps/platform/app/Models/ReviewPack.php](../../apps/platform/app/Models/ReviewPack.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `tenant_review_id`
|
||||||
|
- `status`
|
||||||
|
- `generated_at`
|
||||||
|
- `expires_at`
|
||||||
|
- `summary`
|
||||||
|
- `file_path`
|
||||||
|
- `file_disk`
|
||||||
|
- `sha256`
|
||||||
|
- `operation_run_id`
|
||||||
|
- `tenantReview`
|
||||||
|
- `evidenceSnapshot`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- only current ready, unexpired packs remain available in the customer-safe flow
|
||||||
|
- review-pack access continues to use the existing signed download route and current capability check
|
||||||
|
- the feature must not surface generate/regenerate flows, even when a pack is unavailable
|
||||||
|
|
||||||
|
### Audit Log Event Family
|
||||||
|
|
||||||
|
**Purpose**: Existing auditable truth for explicit customer-review consumption moments.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `audit_logs` rows via [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php)
|
||||||
|
|
||||||
|
**Relevant current action IDs**:
|
||||||
|
- `tenant_review.opened`
|
||||||
|
- `review_pack.downloaded`
|
||||||
|
|
||||||
|
**Potential bounded extensions only if implementation confirms a gap**:
|
||||||
|
- workspace access open event for the customer review workspace route
|
||||||
|
- evidence proof access open event for proof routes launched from the customer review flow
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- auditable access remains on the shared audit path only
|
||||||
|
- no new audit store or mirror analytics stream is justified
|
||||||
|
- workspace, tenant, source-surface, and artifact identifiers stay in stable audit metadata when a new access moment is added
|
||||||
|
|
||||||
|
## Derived Read Models
|
||||||
|
|
||||||
|
### CustomerReviewWorkspaceEntry
|
||||||
|
|
||||||
|
**Purpose**: Derived row-level presentation contract for one entitled tenant on the existing workspace page.
|
||||||
|
|
||||||
|
**Persistence**: none; computed at request time
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `tenant_name`
|
||||||
|
- `latest_published_review_id` (nullable)
|
||||||
|
- `latest_review_published_at` (nullable)
|
||||||
|
- `outcome_summary`
|
||||||
|
- `findings_summary`
|
||||||
|
- `accepted_risk_accountability_summary`
|
||||||
|
- `evidence_proof_state`
|
||||||
|
- `review_pack_state`
|
||||||
|
- `primary_review_url` (nullable)
|
||||||
|
- `review_pack_download_url` (nullable)
|
||||||
|
- `proof_detail_url` (nullable)
|
||||||
|
- `absence_note` (nullable)
|
||||||
|
- `unavailable_note` (nullable)
|
||||||
|
- `redaction_note` (nullable)
|
||||||
|
|
||||||
|
**Derivation rules**:
|
||||||
|
- exactly one derived entry exists per entitled tenant visible in the current workspace scope
|
||||||
|
- if a published review exists, the entry derives its customer-safe summary from that released record only
|
||||||
|
- if no published review exists, the entry surfaces an explicit absence note and omits deep links that depend on a released review
|
||||||
|
- if optional proof or pack access is blocked by capability or artifact state, the review remains readable while the secondary path becomes explicitly unavailable
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- entries may only be built for entitled tenants in the active workspace
|
||||||
|
- `review_pack_download_url` is present only when a current pack exists and the actor can consume it
|
||||||
|
- `proof_detail_url` is present only when the actor can open the proof route
|
||||||
|
- raw payloads, unrestricted diagnostics, provider IDs, and copied support context are never part of the default entry model
|
||||||
|
|
||||||
|
### CustomerReviewDetailPresentation
|
||||||
|
|
||||||
|
**Purpose**: Derived section contract for the existing released-review detail page when it is launched from the customer review workspace.
|
||||||
|
|
||||||
|
**Persistence**: none; computed from the existing review record and current query-context flag
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `review_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `launched_from_customer_workspace` (boolean)
|
||||||
|
- `narrative_outcome_summary`
|
||||||
|
- `findings_summary`
|
||||||
|
- `accepted_risk_accountability_summary`
|
||||||
|
- `evidence_summary`
|
||||||
|
- `proof_pointer_state`
|
||||||
|
- `review_pack_state`
|
||||||
|
- `operator_actions_hidden` (boolean)
|
||||||
|
- `secondary_diagnostics_collapsed` (boolean)
|
||||||
|
|
||||||
|
**Derivation rules**:
|
||||||
|
- only the existing `customer_workspace` query context activates this productized secondary presentation mode
|
||||||
|
- the detail remains readable even when optional pack/evidence capabilities are absent
|
||||||
|
- management actions remain suppressed in this context
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- this derived model must not create a second review detail route or a second stored summary object
|
||||||
|
- secondary proof and support detail remain lower-priority than the narrative governance record
|
||||||
|
- duplicate equal-priority summary blocks between workspace and detail should be removed or reduced
|
||||||
|
|
||||||
|
### CustomerReviewPageState
|
||||||
|
|
||||||
|
**Purpose**: Request/query/session-backed page state already required for tenant-prefilter, remembered scope, and launch context continuity.
|
||||||
|
|
||||||
|
**Persistence**: request, URL query, and existing session-backed table state only
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `tenant` prefilter (nullable)
|
||||||
|
- remembered tenant id in workspace context (nullable)
|
||||||
|
- `customer_workspace` detail context flag (boolean on the detail route)
|
||||||
|
- navigation context metadata when launched from other canonical pages (nullable)
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- explicit tenant prefilters must resolve to an entitled tenant or the request fails as not found
|
||||||
|
- any state required after Livewire interaction must remain hydrated via public/query/session-backed state
|
||||||
|
- no private property may own the control path for disclosure or filter restore
|
||||||
|
|
||||||
|
## Derived Disclosure States
|
||||||
|
|
||||||
|
This feature introduces no new persisted lifecycle or enum family. It does require explicit derived disclosure outcomes on existing surfaces:
|
||||||
|
|
||||||
|
- `available`: the actor can open the review/proof/pack path now
|
||||||
|
- `absent`: the underlying released artifact does not exist for this tenant yet
|
||||||
|
- `unavailable`: the artifact exists conceptually but is not currently consumable because of capability, readiness, or redaction limits
|
||||||
|
- `expired`: the artifact exists and was previously consumable, but time-based or release-lifecycle rules now block access while the surface still needs to explain why
|
||||||
|
- `redacted`: the route or surface remains visible, but protected details stay hidden behind existing redaction rules
|
||||||
|
- `partial`: the governance record is readable, but accountability/proof detail is incomplete in current source truth
|
||||||
|
|
||||||
|
These remain derived page semantics only and must not become stored status families.
|
||||||
|
|
||||||
|
## State Transition Summary
|
||||||
|
|
||||||
|
No new persisted lifecycle is added. Only derived surface transitions are expected:
|
||||||
|
|
||||||
|
- workspace open -> entitled tenant rows or truthful empty/absence state
|
||||||
|
- remembered tenant or explicit tenant query -> tenant-prefiltered workspace view
|
||||||
|
- workspace row with released review -> existing review detail route available
|
||||||
|
- workspace row without released review -> explicit absence state and no review-open action
|
||||||
|
- released review detail with optional proof/pack capability missing -> review remains readable and secondary path becomes unavailable
|
||||||
|
- released review detail with an expired pack/proof artifact -> review remains readable and secondary path becomes explicitly expired
|
||||||
|
- explicit workspace/review/proof/pack consumption -> shared audit event when covered by the current audit registry or a bounded additive action ID
|
||||||
301
specs/258-customer-review-productization/plan.md
Normal file
301
specs/258-customer-review-productization/plan.md
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
# Implementation Plan: Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Branch**: `258-customer-review-productization` | **Date**: 2026-04-30 | **Spec**: [spec.md](spec.md)
|
||||||
|
**Input**: Feature specification from [spec.md](spec.md)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Productize the existing customer review workspace into a calmer, customer-safe governance-of-record surface by tightening the current [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) page and the existing released-review drilldown in [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php). The implementation should reuse current review, finding, accepted-risk, evidence, review-pack, localization, RBAC, and audit truth rather than adding a portal shell, new persistence, or a second presentation framework.
|
||||||
|
|
||||||
|
This is a bounded follow-up to Spec 249, not a fresh workspace foundation. Filament remains on Livewire v4 under v5, panel-provider registration stays where it is today in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no new panel or provider work is planned, no new globally searchable scope is introduced, no destructive actions are in scope, and no new asset registration strategy is expected.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4, Laravel 12
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing review/evidence/review-pack services, capability helpers, localization copy, and workspace audit infrastructure
|
||||||
|
**Storage**: PostgreSQL via existing `tenant_reviews`, `review_packs`, `evidence_snapshots`, `finding_exceptions`, memberships, and `audit_logs`; no new persistence planned
|
||||||
|
**Testing**: Pest v4 feature coverage plus one bounded browser smoke slice on the existing workspace flow
|
||||||
|
**Validation Lanes**: confidence, browser
|
||||||
|
**Target Platform**: Laravel monolith in `apps/platform`, existing admin plane only (`/admin` plus existing tenant-scoped `/admin/t/{tenant}` reuse)
|
||||||
|
**Project Type**: Web application (Laravel monolith with Filament pages/resources)
|
||||||
|
**Performance Goals**: keep workspace and detail rendering DB-only and scope-safe, reuse eager-loaded existing review/pack/evidence relations, and avoid any new Graph calls, queue starts, or heavy asset work on render
|
||||||
|
**Constraints**: no new page shell, no new persistence, no review publishing engine, no remediation flow, no new customer identity plane, no new global-search scope, no new heavy asset strategy, and no destructive action exposure
|
||||||
|
**Scale/Scope**: 1 existing workspace page, 1 existing released-review detail page, 2 existing proof/detail resources, 2 localization files, 1 shared audit pipeline, and the existing `tests/Feature/Reviews/*` plus 1 existing browser smoke
|
||||||
|
|
||||||
|
## Likely Affected Repo Surfaces
|
||||||
|
|
||||||
|
- [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) for calmer workspace copy, derived summary semantics, explicit access or absence states, and table action hierarchy.
|
||||||
|
- [../../apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php](../../apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php) for the page intro and disclosure framing.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) for the released-review secondary-context contract, customer-workspace query-flag behavior, and audit handoff.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/TenantReviewResource.php](../../apps/platform/app/Filament/Resources/TenantReviewResource.php) for existing released-review detail sections, proof links, and current pack/evidence affordances.
|
||||||
|
- [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php) for workspace membership, entitled tenant scoping, and latest-published review composition.
|
||||||
|
- [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) plus `SurfaceCompressionContext` for outcome, freshness, and publication wording already used by review and pack surfaces.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/ReviewPackResource.php](../../apps/platform/app/Filament/Resources/ReviewPackResource.php), [../../apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php](../../apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php), and [../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php](../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php) for current pack availability, safe deep links, and signed download auditing.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php) and [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php) for proof-pointer routing and explicit unavailable states.
|
||||||
|
- [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) and [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) for auditable workspace access, review access, proof access, and pack download behavior through the shared audit path.
|
||||||
|
- [../../apps/platform/app/Services/Auth/RoleCapabilityMap.php](../../apps/platform/app/Services/Auth/RoleCapabilityMap.php) and [../../apps/platform/app/Support/Auth/Capabilities.php](../../apps/platform/app/Support/Auth/Capabilities.php) for capability-first RBAC and workspace/tenant-safe omission rules.
|
||||||
|
- [../../apps/platform/lang/en/localization.php](../../apps/platform/lang/en/localization.php) and [../../apps/platform/lang/de/localization.php](../../apps/platform/lang/de/localization.php) for calmer customer-safe wording without introducing a second vocabulary system.
|
||||||
|
- [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php), and [../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php](../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php) for the bounded proof surface already in the repo.
|
||||||
|
|
||||||
|
## UI / Filament & Livewire Fit
|
||||||
|
|
||||||
|
- Keep [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) as the canonical customer-safe landing surface. This follow-up productizes the existing page instead of adding a new page class, a new Resource, or a new panel.
|
||||||
|
- Keep [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) as the secondary context surface reached from the workspace via the existing `customer_workspace` query flag. The drilldown should deepen the governance record, not reopen operator lifecycle controls.
|
||||||
|
- Preserve the current Livewire-safe filter and remembered-tenant behavior already implemented on the workspace page. Any added state must remain public, query-backed, or session-backed; no private state should control postback-critical disclosure.
|
||||||
|
- Retain one dominant next action per surface. On the workspace page that remains `Open released review`; pack download or proof routes stay secondary and capability-gated. On the detail page, review-pack access remains the dominant safe action while evidence proof stays lower priority.
|
||||||
|
- Keep the entire feature in native Filament primitives plus the existing review/evidence shared seams. No custom shell, no heavy asset registration, and no new global-search scope are planned.
|
||||||
|
|
||||||
|
## RBAC / Policy Fit
|
||||||
|
|
||||||
|
- Workspace membership remains the first isolation boundary through the existing workspace context and `TenantReviewRegisterService::canAccessWorkspace(...)` path.
|
||||||
|
- Entitled-tenant composition remains capability-first: page entry and rows continue to derive from the current role-capability map and `TENANT_REVIEW_VIEW` path rather than new customer-only roles or raw role-string checks.
|
||||||
|
- Proof pointers and safe secondary actions continue to reuse existing gates: `REVIEW_PACK_VIEW` for current pack download, `EVIDENCE_VIEW` for proof detail, `TENANT_FINDINGS_VIEW` and `FINDING_EXCEPTION_VIEW` for deeper review content when surfaced, and existing policy checks on review/evidence resources.
|
||||||
|
- Non-members and explicit out-of-scope tenant targets remain `404`. Member actors who can read the review surface but lack an optional deep-link capability should still see the review with an explicit unavailable state for that optional path.
|
||||||
|
- No new panel, tenant plane, customer portal plane, or identity model is introduced. This remains an admin-plane follow-up only.
|
||||||
|
|
||||||
|
## Audit / Logging Fit
|
||||||
|
|
||||||
|
- Reuse [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) and [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) for all auditable moments. No new audit store, no telemetry sidecar, and no page-local logging subsystem are justified.
|
||||||
|
- Current review access from the customer-workspace drilldown already logs `TenantReviewOpened` with `source_surface=customer_review_workspace`, and current pack downloads already log `ReviewPackDownloaded` through the signed download route.
|
||||||
|
- Planning should explicitly account for two remaining audit moments required by this spec: workspace access itself and evidence-summary or proof access when the actor opens an explicit proof route from the customer-safe flow. If those moments are not already covered, the narrowest acceptable change is additive stable action IDs on the existing audit pipeline.
|
||||||
|
- Passive rendering should still avoid noisy event spam. The auditable boundary is explicit workspace entry or explicit artifact/proof consumption, not every Livewire repaint.
|
||||||
|
|
||||||
|
## Data & Query Fit
|
||||||
|
|
||||||
|
- Keep the base row query on the existing `customerWorkspaceTenantQuery(...)` seam in [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php). This feature productizes the presentation contract over that query; it does not replace it with a new projection.
|
||||||
|
- Findings and accepted-risk summaries remain derived from the current `TenantReview.summary` payload already used by the workspace page, including `finding_count`, `finding_outcomes`, and `risk_acceptance` substructures.
|
||||||
|
- Accepted-risk accountability follow-through should reuse existing `FindingException` and current decision truth where that data already exists. Missing accountable-person or accountable-role truth must surface as explicit partial or unavailable disclosure, not invented copy.
|
||||||
|
- Evidence proof semantics should stay anchored to existing `EvidenceSnapshot`, related context entries, and `ArtifactTruthPresenter` output. The feature may reorder or reword disclosure, but it should not create a second evidence summary model.
|
||||||
|
- Access, absence, unavailable, expired, and redacted states remain derived UI or route-state semantics only. They must not become new persisted lifecycle fields or a new presentation enum family.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: changed surfaces
|
||||||
|
- **Native vs custom classification summary**: native Filament
|
||||||
|
- **Shared-family relevance**: status messaging, evidence/report viewers, action links, navigation entry points, access-state messaging, and review-pack access affordances
|
||||||
|
- **State layers in scope**: page, detail, URL-query, table/session restore
|
||||||
|
- **Audience modes in scope**: customer/read-only, customer-admin, auditor-read-only, operator-MSP
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
|
||||||
|
- **Raw/support gating plan**: collapsed and capability-gated on reused detail/proof routes only
|
||||||
|
- **One-primary-action / duplicate-truth control**: `Open released review` remains the workspace primary action; review-pack access is the detail primary action; equal-priority duplicate summary blocks across workspace and detail are out of scope
|
||||||
|
- **Handling modes by drift class or surface**: review-mandatory
|
||||||
|
- **Repository-signal treatment**: review-mandatory
|
||||||
|
- **Special surface test profiles**: standard-native-filament, shared-detail-family
|
||||||
|
- **Required tests or manual smoke**: functional-core, state-contract, bounded-browser-smoke
|
||||||
|
- **Exception path and spread control**: none planned; any new presenter/taxonomy/customer-shell proposal becomes exception-required drift
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes
|
||||||
|
- **Systems touched**: `CustomerReviewWorkspace`, `ViewTenantReview`, `TenantReviewResource`, `ReviewPackResource`, `EvidenceSnapshotResource`, `TenantReviewRegisterService`, `ArtifactTruthPresenter`, `SurfaceCompressionContext`, `ActionSurfaceDeclaration`, `ReviewPackDownloadController`, `WorkspaceAuditLogger`, `AuditActionId`, and review localization copy
|
||||||
|
- **Shared abstractions reused**: `TenantReviewRegisterService`, `ArtifactTruthPresenter`, `SurfaceCompressionContext`, existing resource URL helpers, existing action-surface declarations, `ReviewPackService`, and the shared audit logger
|
||||||
|
- **New abstraction introduced? why?**: none planned. If implementation discovers a small copy or disclosure helper is needed, it should stay inside the existing review surface family instead of becoming a new reusable framework
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: the repo already has the page, detail route, truth envelopes, pack download path, and audit seams; what is insufficient today is the product contract over those seams, not the underlying domain model
|
||||||
|
- **Bounded deviation / spread control**: none planned. This slice should tighten the current path rather than add a parallel customer-review language, mirror page, or publication layer
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no
|
||||||
|
- **Central contract reused**: `N/A`
|
||||||
|
- **Delegated UX behaviors**: `N/A`
|
||||||
|
- **Surface-owned behavior kept local**: read-only workspace and detail rendering only; any existing operation-run links remain secondary diagnostics on reused detail surfaces
|
||||||
|
- **Queued DB-notification policy**: `N/A`
|
||||||
|
- **Terminal notification path**: `N/A`
|
||||||
|
- **Exception path**: none
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: no
|
||||||
|
- **Provider-owned seams**: `N/A`
|
||||||
|
- **Platform-core seams**: existing workspace, tenant, review, evidence, risk acceptance, review pack, and audit vocabulary only
|
||||||
|
- **Neutral platform terms / contracts preserved**: `workspace`, `tenant`, `review`, `evidence`, `review pack`, `accepted risk`, `proof`, and existing artifact-truth wording
|
||||||
|
- **Retained provider-specific semantics and why**: none new
|
||||||
|
- **Bounded extraction or follow-up path**: `N/A`
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before implementation preparation continues. Re-check after Phase 1 design artifacts.*
|
||||||
|
|
||||||
|
- Inventory-first / snapshot truth: PASS. The slice consumes existing review, pack, and evidence artifacts as read-only truth.
|
||||||
|
- Read/write separation: PASS. No new create, publish, regenerate, refresh, remediation, or destructive flow is introduced.
|
||||||
|
- Graph contract path: PASS. No new Graph work or provider contract work is part of this slice.
|
||||||
|
- Deterministic capabilities: PASS. Existing capability registries and role maps remain authoritative.
|
||||||
|
- Workspace and tenant isolation: PASS. Workspace membership and tenant entitlement remain non-negotiable `404` boundaries.
|
||||||
|
- RBAC-UX plane separation: PASS. Everything stays in the existing `/admin` plane and current tenant-scoped detail routes.
|
||||||
|
- Destructive confirmation standard: PASS by non-use. Destructive actions are out of scope.
|
||||||
|
- Global search safety: PASS. No new globally searchable resource or search scope is added; any mention of search remains tenant-safe reuse only.
|
||||||
|
- OperationRun / Ops-UX: PASS by non-use. The productization slice starts no runs and changes no run lifecycle UX.
|
||||||
|
- Data minimization: PASS. Default-visible content remains decision-first; raw payloads and unrestricted diagnostics stay gated.
|
||||||
|
- Test governance (TEST-GOV-001): PASS. Planned proof stays in focused feature coverage plus one bounded browser smoke.
|
||||||
|
- Proportionality / no premature abstraction: PASS. The feature productizes existing surfaces instead of adding persistence, a shell, or a second presenter framework.
|
||||||
|
- Persisted truth (PERSIST-001): PASS. No new table, artifact, or cache is planned.
|
||||||
|
- Behavioral state (STATE-001): PASS. Access, absence, unavailable, expired, and redacted conditions remain derived presentation semantics.
|
||||||
|
- UI semantics / shared pattern first / Filament-native UI: PASS. Native Filament pages/resources and existing truth abstractions remain the default path.
|
||||||
|
- Provider boundary (PROV-001): PASS. No provider/platform seam widens.
|
||||||
|
- Filament / Laravel planning contract: PASS. Filament v5 stays on Livewire v4, provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no new panel/provider work is planned, no new global-search scope is created, and asset handling stays unchanged (`cd apps/platform && php artisan filament:assets` remains deploy-only if future registered assets are ever added).
|
||||||
|
|
||||||
|
**Gate evaluation**: PASS.
|
||||||
|
|
||||||
|
- The feature stays in the existing admin plane and current workspace/tenant membership model.
|
||||||
|
- The canonical entry surface remains the existing customer review workspace, not a new shell.
|
||||||
|
- Existing truth seams are sufficient if implementation resists adding a mirror presenter or publication engine.
|
||||||
|
|
||||||
|
**Post-design re-check**: PASS (design artifacts: [research.md](research.md), [data-model.md](data-model.md), [quickstart.md](quickstart.md), [contracts/customer-review-productization.openapi.yaml](contracts/customer-review-productization.openapi.yaml)).
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: Feature for workspace rows, access/absence/unavailable states, navigation context, pack access, and audit metadata; Browser for one bounded end-to-end calm disclosure path on the existing workspace handoff
|
||||||
|
- **Affected validation lanes**: confidence, browser
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: the repo already has the exact workspace feature family and a single smoke harness; expanding those files is cheaper and more honest than adding new browser families or generalized helpers
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: moderate but contained; reuse existing workspace membership, entitled-tenant, published review, finding-exception, evidence snapshot, review pack, and audit fixtures
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no; any new helper should stay explicit and inside the reviews family
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none beyond the already-existing single browser smoke
|
||||||
|
- **Surface-class relief / special coverage rule**: standard-native-filament relief on the workspace page, shared-detail-family coverage on the released-review handoff
|
||||||
|
- **Closing validation and reviewer handoff**: rerun the focused commands above, verify customer-safe default visibility, verify `404` on out-of-scope tenant targeting, verify optional proof paths show explicit unavailable states instead of leaking content, and verify audit metadata stays on the shared logger path
|
||||||
|
- **Budget / baseline / trend follow-up**: none expected beyond small feature-local assertions in the existing reviews suite
|
||||||
|
- **Review-stop questions**: lane fit, hidden fixture growth, browser sprawl, duplicate-truth regressions, audit-gap drift
|
||||||
|
- **Escalation path**: `document-in-feature` for contained audit metadata placement notes; `reject-or-split` for any drift into new persistence, portal scope, or expanded browser coverage
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||||
|
- **Why no dedicated follow-up spec is needed**: this is already the bounded follow-up to Spec 249; remaining work stays inside this productization lane unless it tries to become publishing/remediation/portal scope
|
||||||
|
|
||||||
|
## Rollout & Risk Controls
|
||||||
|
|
||||||
|
- Keep the canonical entry surface on the existing workspace page and the canonical secondary surface on the existing released-review detail route.
|
||||||
|
- Keep all proof and packaged artifact flows on the existing tenant review, review-pack, and evidence routes. Do not add a new proof viewer or download endpoint.
|
||||||
|
- Treat missing accountability truth or missing proof availability as explicit partial or unavailable disclosure, never as fabricated customer-safe copy.
|
||||||
|
- Prefer localization-key updates in the existing review language namespace over page-local inline wording.
|
||||||
|
- Keep browser validation bounded to the existing smoke harness before considering any wider UI rollout.
|
||||||
|
|
||||||
|
## Guardrail / Smoke Coverage Close-Out
|
||||||
|
|
||||||
|
- **Close-out date**: 2026-04-30
|
||||||
|
- **Confidence lane**: PASS via the focused customer-workspace, TenantReview detail, ReviewPack, EvidenceSnapshot, audit, capability, and download feature suites listed in [quickstart.md](quickstart.md).
|
||||||
|
- **Browser lane**: PASS via the bounded customer-review workspace smoke test. Tested path: `/admin/reviews/workspace` as a readonly-capable actor, released review row visibility, customer-safe pack/proof availability labels, workspace-to-detail handoff, and customer-safe released-review detail text.
|
||||||
|
- **Audit-gap outcome**: bounded additive action IDs were required for explicit workspace entry and proof-open events (`customer_review_workspace.opened`, `evidence_snapshot.opened`). Existing `tenant_review.opened` and `review_pack.downloaded` paths were reused with `source_surface=customer_review_workspace`.
|
||||||
|
- **Localization / copy outcome**: contained to the existing review localization namespace in English and German; no new vocabulary framework or page-local copy layer was introduced.
|
||||||
|
- **Global-search safety outcome**: no new globally searchable resource or search scope was introduced. Touched review, pack, and evidence resources remain on their existing tenant-scoped resource paths and customer-workspace query context.
|
||||||
|
- **Follow-up decision**: no `follow-up-spec` is required for the implemented scope. Broader portal, publication, remediation, baseline/control overlays, and management-packaging expansion remain outside this feature.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/258-customer-review-productization/
|
||||||
|
├── checklists/
|
||||||
|
│ └── requirements.md
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── customer-review-productization.openapi.yaml
|
||||||
|
└── tasks.md # Created later by /speckit.tasks, not by this plan step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ ├── Pages/Reviews/
|
||||||
|
│ │ │ └── CustomerReviewWorkspace.php
|
||||||
|
│ │ └── Resources/
|
||||||
|
│ │ ├── TenantReviewResource.php
|
||||||
|
│ │ ├── TenantReviewResource/Pages/ViewTenantReview.php
|
||||||
|
│ │ ├── ReviewPackResource.php
|
||||||
|
│ │ ├── ReviewPackResource/Pages/ViewReviewPack.php
|
||||||
|
│ │ ├── EvidenceSnapshotResource.php
|
||||||
|
│ │ └── EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php
|
||||||
|
│ ├── Http/Controllers/ReviewPackDownloadController.php
|
||||||
|
│ ├── Models/
|
||||||
|
│ │ ├── TenantReview.php
|
||||||
|
│ │ ├── ReviewPack.php
|
||||||
|
│ │ ├── EvidenceSnapshot.php
|
||||||
|
│ │ └── FindingException.php
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── Audit/WorkspaceAuditLogger.php
|
||||||
|
│ │ └── TenantReviews/TenantReviewRegisterService.php
|
||||||
|
│ ├── Support/
|
||||||
|
│ │ ├── Audit/AuditActionId.php
|
||||||
|
│ │ ├── Auth/Capabilities.php
|
||||||
|
│ │ └── Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
|
||||||
|
├── lang/
|
||||||
|
│ ├── de/localization.php
|
||||||
|
│ └── en/localization.php
|
||||||
|
├── bootstrap/providers.php
|
||||||
|
├── resources/views/filament/pages/reviews/customer-review-workspace.blade.php
|
||||||
|
└── tests/
|
||||||
|
├── Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php
|
||||||
|
├── Feature/ReviewPack/ReviewPackDownloadTest.php
|
||||||
|
└── Feature/Reviews/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Laravel monolith. The implementation stays inside the existing `apps/platform` reviews, review-pack, evidence, localization, and audit surfaces, with no new panel/provider locations and no new persistence layer.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| None expected at planning time | The intended implementation is a productization pass over existing pages, routes, copy, and audit seams | Adding a portal, new presenter layer, or persisted customer-review projection would import unnecessary structure |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: the repo already has customer-review truth, but the current workspace and drilldown still feel too operator-led and under-explain accountability, proof, and unavailable states for a customer-safe governance record.
|
||||||
|
- **Existing structure is insufficient because**: Spec 249 created the canonical entry route, but the product contract across workspace summary, released-review detail, proof pointers, pack access, and auditable access semantics is still incomplete.
|
||||||
|
- **Narrowest correct implementation**: tighten the existing workspace page, released-review detail, proof affordances, localization copy, and shared audit metadata without adding a new page shell, persistence, or customer-specific presenter family.
|
||||||
|
- **Ownership cost created**: limited copy/disclosure maintenance on existing surfaces, a small extension to focused tests, and at most bounded additive audit action IDs if current coverage is incomplete.
|
||||||
|
- **Alternative intentionally rejected**: a new portal, publication engine, remediation flow, or second customer-review explanation framework was rejected because the repo already has the required read-only truth seams.
|
||||||
|
- **Release truth**: current-release productization follow-up to Spec 249.
|
||||||
|
|
||||||
|
## Phase 0 — Research (output: research.md)
|
||||||
|
|
||||||
|
Research resolves the remaining implementation-shaping decisions:
|
||||||
|
|
||||||
|
- keep the existing `CustomerReviewWorkspace` page as the canonical customer-safe landing surface
|
||||||
|
- keep `ViewTenantReview` as the secondary detail surface under the current `customer_workspace` query flag
|
||||||
|
- reuse existing localization, artifact-truth, and accepted-risk seams instead of adding a second vocabulary
|
||||||
|
- keep workspace and tenant isolation on the current capability-first RBAC paths
|
||||||
|
- reuse the existing audit pipeline and identify only the bounded missing access moments that may need additive action IDs
|
||||||
|
- keep browser coverage bounded to the existing workspace smoke path and focused feature tests
|
||||||
|
|
||||||
|
**Output**: [research.md](research.md)
|
||||||
|
|
||||||
|
## Phase 1 — Design (outputs: data-model.md, contracts/, quickstart.md)
|
||||||
|
|
||||||
|
Design artifacts capture the narrow productization shape:
|
||||||
|
|
||||||
|
- no new persistence; reused truth stays in tenant reviews, finding exceptions, review packs, evidence snapshots, memberships, and audit logs
|
||||||
|
- one derived workspace presentation contract and one derived released-review disclosure contract document the existing surfaces without becoming stored entities
|
||||||
|
- the conceptual contract documents current workspace, review detail, proof, and pack-download route expectations plus explicit access/absence/unavailable semantics
|
||||||
|
- quickstart records the intended implementation order, bounded validation commands, Filament v5 / Livewire v4 posture, provider-registration location, and no-new-assets expectation
|
||||||
|
|
||||||
|
**Artifacts**:
|
||||||
|
|
||||||
|
- [data-model.md](data-model.md)
|
||||||
|
- [contracts/customer-review-productization.openapi.yaml](contracts/customer-review-productization.openapi.yaml)
|
||||||
|
- [quickstart.md](quickstart.md)
|
||||||
|
|
||||||
|
## Phase 2 — Planning (for tasks.md)
|
||||||
|
|
||||||
|
Dependency-ordered implementation outline for the later `tasks.md` step:
|
||||||
|
|
||||||
|
1. Tighten the existing workspace page and Blade intro so the default-visible path is calm, customer-safe, and explicit about absence/unavailable states.
|
||||||
|
2. Tighten the existing released-review detail flow under the `customer_workspace` context flag so it remains read-only and deepens understanding without exposing operator lifecycle actions.
|
||||||
|
3. Reuse existing review summary, finding outcome, accepted-risk, proof, and pack truth to improve explanation quality and customer-safe disclosure hierarchy without adding a second presenter or new persistence.
|
||||||
|
4. Align proof pointers and review-pack affordances so optional deep links are capability-gated and unavailable states are explicit.
|
||||||
|
5. Reuse the shared audit pipeline for workspace access, review access, proof access, and pack downloads, adding only bounded audit registry entries if the current actions do not cover required moments.
|
||||||
|
6. Expand the focused review feature suite and keep the single existing browser smoke as the only browser proof for this slice.
|
||||||
|
|
||||||
|
## Planning Guardrail Notes
|
||||||
|
|
||||||
|
- Planning guardrail result: PASS. Filament remains v5 on Livewire v4, panel providers remain in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no new global-search scope is introduced, no destructive action is added, and no new asset bundle is planned.
|
||||||
|
- Shared seam result: the plan stays on existing page/resource/service/audit seams, not a new customer-review framework.
|
||||||
|
- Smoke plan: the existing [../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php](../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php) remains the single bounded browser proof.
|
||||||
|
- Agent context update: intentionally skipped during this plan pass because the feature introduces no new technology and the user requested preparation-artifact-only changes.
|
||||||
55
specs/258-customer-review-productization/quickstart.md
Normal file
55
specs/258-customer-review-productization/quickstart.md
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# Quickstart — Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- Docker is running and the Sail stack for `apps/platform` is available.
|
||||||
|
- The feature remains inside the existing Laravel monolith and existing admin plane.
|
||||||
|
- The canonical entry surface already exists at `/admin/reviews/workspace`; this slice productizes it instead of adding a new shell.
|
||||||
|
- No new persistence, no review publishing engine, no remediation flow, no new identity plane, and no heavy new asset strategy are part of this work.
|
||||||
|
|
||||||
|
## Intended Implementation Order
|
||||||
|
|
||||||
|
1. Review the current workspace page, Blade intro, and feature/browser tests so the productization pass stays inside the existing reviews family.
|
||||||
|
2. Tighten the workspace page wording, disclosure order, and explicit access/absence/unavailable states using the existing localization namespace in [../../apps/platform/lang/en/localization.php](../../apps/platform/lang/en/localization.php) and [../../apps/platform/lang/de/localization.php](../../apps/platform/lang/de/localization.php).
|
||||||
|
3. Tighten the released-review detail flow in [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) under the existing `customer_workspace` context flag so it remains read-only and customer-safe.
|
||||||
|
4. Reuse the current `TenantReview.summary`, `FindingException`, `ArtifactTruthPresenter`, review-pack, and evidence seams to improve accountability/proof framing without creating a new presenter or persistence layer.
|
||||||
|
5. Align secondary proof and pack affordances so the workspace still has one dominant next action and optional proof paths show explicit unavailable or expired states when blocked.
|
||||||
|
6. Reuse the shared audit pipeline for workspace access, review access, proof access, and pack download moments, adding only bounded action IDs if the current registry does not already cover the required events.
|
||||||
|
7. Expand the focused `tests/Feature/Reviews/*` family and keep the existing browser smoke as the only browser proof for this slice.
|
||||||
|
8. Run the targeted tests and Pint after implementation.
|
||||||
|
|
||||||
|
## Targeted Validation Commands (after implementation)
|
||||||
|
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
|
||||||
|
## Planned Smoke Checklist (after implementation)
|
||||||
|
|
||||||
|
1. Sign in to `/admin` as a readonly-capable actor with workspace scope and open `/admin/reviews/workspace`.
|
||||||
|
2. Confirm the page stays calm and customer-safe: current governance record first, no mutation actions, and explicit absence/unavailable states where appropriate.
|
||||||
|
3. Launch the workspace from an existing released review or related context and confirm tenant prefilter and customer-safe drilldown continuity still hold.
|
||||||
|
4. Open the released review and confirm the detail stays read-only, highlights findings/accepted-risk/accountability/proof clearly, and does not expose publish/refresh/create-next/regenerate/archive controls.
|
||||||
|
5. Use the pack action for a tenant with a current pack and confirm the existing signed download path still works; for tenants without a current or still-valid pack, confirm the UI shows a truthful unavailable or expired state instead of a generation action.
|
||||||
|
6. Follow an optional proof path and confirm the route is capability-gated, auditable when required, and explicit when proof is unavailable or redacted.
|
||||||
|
7. Attempt an explicit out-of-scope tenant target and confirm the result remains not found without leaking tenant presence.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Filament v5 already runs on Livewire v4 in this repo.
|
||||||
|
- Panel providers remain registered through [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php); this slice does not add or move providers.
|
||||||
|
- No new globally searchable resource or search scope is part of this productization pass.
|
||||||
|
- No destructive action belongs on the workspace surface or the customer-workspace drilldown. If implementation accidentally exposes one, it must stay out of scope and use confirmation.
|
||||||
|
- No new registered asset bundle is expected. If future implementation unexpectedly registers a Filament asset, deployment still requires `cd apps/platform && php artisan filament:assets`.
|
||||||
|
- This remains a customer-safe consumption/productization slice only. Review creation, publication, regeneration, remediation, and broader portal behavior stay outside this spec.
|
||||||
|
|
||||||
|
## Implementation Close-Out
|
||||||
|
|
||||||
|
- **Completed**: 2026-04-30
|
||||||
|
- **Targeted feature checks**: PASS
|
||||||
|
- **Browser smoke**: PASS, covering `/admin/reviews/workspace`, released-review row visibility, customer-safe pack/proof labels, workspace-to-detail handoff, and released-governance-record detail text.
|
||||||
|
- **Formatting**: PASS via Pint dirty-file run.
|
||||||
|
- **Audit result**: used bounded additive action IDs only for the confirmed gaps (`customer_review_workspace.opened`, `evidence_snapshot.opened`); reused existing tenant-review open and review-pack download audit events with `source_surface=customer_review_workspace`.
|
||||||
|
- **Global-search result**: unchanged; this implementation added no global-search surface.
|
||||||
|
- **Assets / deploy result**: unchanged; no new Filament assets were registered.
|
||||||
156
specs/258-customer-review-productization/research.md
Normal file
156
specs/258-customer-review-productization/research.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# Research — Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Date**: 2026-04-30
|
||||||
|
**Spec**: [spec.md](spec.md)
|
||||||
|
|
||||||
|
This document resolves the planning decisions for the smallest safe productization follow-up to Spec 249.
|
||||||
|
|
||||||
|
## Decision 1 — Keep the existing customer review workspace as the canonical landing surface
|
||||||
|
|
||||||
|
**Decision**: Productize the existing [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) page instead of creating a second customer-review page, new Resource, new panel, or customer portal shell.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The repo already has the canonical admin-plane route, current tenant prefilter behavior, and bounded test family for this page.
|
||||||
|
- The gap is productization of the current disclosure contract, not missing routing or missing persistence.
|
||||||
|
- Reusing the existing page keeps the follow-up aligned with Spec 249 and avoids a second customer-review vocabulary.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a second customer-facing page or shell.
|
||||||
|
- Rejected: duplicates the existing workspace route and widens scope into shell-level IA.
|
||||||
|
- Convert the workspace into a new Resource.
|
||||||
|
- Rejected: this is still a read-only workspace report, not a new persisted object family.
|
||||||
|
|
||||||
|
## Decision 2 — Keep the existing released-review detail route as the only secondary context surface
|
||||||
|
|
||||||
|
**Decision**: Continue to use [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) as the secondary context surface reached from the workspace by the existing `customer_workspace` query flag.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The current detail page already suppresses management actions when launched from the customer-workspace flow.
|
||||||
|
- The current route already writes customer-workspace review-open audit metadata.
|
||||||
|
- Productization should deepen understanding on the current drilldown path instead of inventing a second detail page.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a customer-only review detail page.
|
||||||
|
- Rejected: would duplicate detail truth and drift from the current policy/audit path.
|
||||||
|
- Push all new explanation back onto the workspace page only.
|
||||||
|
- Rejected: would keep the first drilldown operator-heavy and incomplete.
|
||||||
|
|
||||||
|
## Decision 3 — Reuse the existing review summary and artifact-truth seams for findings, accepted-risk, and proof framing
|
||||||
|
|
||||||
|
**Decision**: Reuse the current `TenantReview.summary` payload, [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php), and current review/evidence/pack relationships for customer-safe findings, accepted-risk, and proof framing.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The workspace page already derives `finding_outcomes` and `risk_acceptance` summary content from the current review payload.
|
||||||
|
- `ArtifactTruthPresenter` already normalizes review and pack freshness/publication semantics.
|
||||||
|
- The productization gap is wording, priority, and explicit unavailable-state behavior, not missing domain truth.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Introduce a customer-review presenter or new derived persistence layer.
|
||||||
|
- Rejected: adds structure without new source-of-truth need.
|
||||||
|
- Inline a second page-local taxonomy for findings and proof states.
|
||||||
|
- Rejected: creates shared-language drift across workspace, review, and pack surfaces.
|
||||||
|
|
||||||
|
## Decision 4 — Keep accepted-risk accountability rooted in existing finding-exception truth
|
||||||
|
|
||||||
|
**Decision**: Accepted-risk accountability remains derived from existing `FindingException` and current decision truth where available; missing accountable-person or accountable-role data must surface as partial/unavailable disclosure rather than a fabricated customer-safe summary.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The spec explicitly forbids new persistence and new decision stores.
|
||||||
|
- Existing `FindingException` truth already includes owner, approver, current decision, validity, review due, and evidence reference relationships.
|
||||||
|
- Productization requires better accountability framing, but not a parallel accepted-risk model.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a new customer-accountability projection.
|
||||||
|
- Rejected: violates the no-new-persistence goal.
|
||||||
|
- Hide accepted-risk accountability when details are incomplete.
|
||||||
|
- Rejected: weakens the governance-of-record objective and obscures truthful partiality.
|
||||||
|
|
||||||
|
## Decision 5 — Keep workspace and tenant isolation on the current capability-first seams
|
||||||
|
|
||||||
|
**Decision**: Reuse [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php), [../../apps/platform/app/Services/Auth/RoleCapabilityMap.php](../../apps/platform/app/Services/Auth/RoleCapabilityMap.php), and [../../apps/platform/app/Support/Auth/Capabilities.php](../../apps/platform/app/Support/Auth/Capabilities.php) as the isolation and entitlement seams.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The current workspace page already uses `canAccessWorkspace(...)`, `authorizedTenants(...)`, and the current workspace context.
|
||||||
|
- The existing test family already proves `404` semantics for out-of-scope tenant targeting on the workspace route.
|
||||||
|
- Capability-first reuse avoids new role families or customer-only policy forks.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add new customer-review roles or customer-specific policy branches.
|
||||||
|
- Rejected: outside scope and unnecessary for the current admin-plane audience.
|
||||||
|
- Resolve optional proof access by hiding the entire review.
|
||||||
|
- Rejected: the review should remain readable even when optional proof is unavailable.
|
||||||
|
|
||||||
|
## Decision 6 — Treat access, absence, unavailable, expired, and redacted conditions as derived disclosure states only
|
||||||
|
|
||||||
|
**Decision**: The follow-up should make access, absence, unavailable, expired, and redacted conditions explicit across workspace, review, evidence, and pack paths, but these remain derived view semantics rather than new persisted statuses.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The current workspace page already distinguishes `No published review available yet` and pack availability from persisted review lifecycle state.
|
||||||
|
- The spec explicitly rejects new state families and new persistence.
|
||||||
|
- Explicit customer-safe disclosure is the sellability gap; a new persisted taxonomy is not.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a new customer-availability enum family.
|
||||||
|
- Rejected: presentation-only distinction with no new lifecycle consequence.
|
||||||
|
- Leave absence and unavailable states implicit.
|
||||||
|
- Rejected: that is the current productization gap.
|
||||||
|
|
||||||
|
## Decision 7 — Reuse the current review-pack and evidence proof routes instead of adding new proof viewers
|
||||||
|
|
||||||
|
**Decision**: Keep [../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php](../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php), [../../apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php](../../apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php), and [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php) as the only proof/detail routes reused by this feature.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Current pack download already enforces tenant access, capability checks, readiness, expiry, and audit logging.
|
||||||
|
- Current evidence resource routing already exists and is referenced by review and pack truth presenters.
|
||||||
|
- The follow-up only needs clearer proof-pointer semantics and explicit unavailable states, not a new viewer family.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a new customer-proof page or customer-only download endpoint.
|
||||||
|
- Rejected: duplicates existing review-pack and evidence seams.
|
||||||
|
- Surface raw evidence payloads directly on the workspace page.
|
||||||
|
- Rejected: violates customer-safe disclosure hierarchy.
|
||||||
|
|
||||||
|
## Decision 8 — Reuse the existing audit pipeline and extend it only where access moments are still missing
|
||||||
|
|
||||||
|
**Decision**: Keep all auditing on [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) and [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php). Existing review-open and pack-download actions stay authoritative, and only bounded additive action IDs should be considered if workspace access or proof access moments are not yet covered.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The current customer-workspace review handoff already logs `TenantReviewOpened`.
|
||||||
|
- The current signed pack download route already logs `ReviewPackDownloaded`.
|
||||||
|
- The repo does not currently show a matching evidence-proof-open audit on the customer-workspace flow, so that is the bounded gap to evaluate.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Create a new customer-review audit table or analytics stream.
|
||||||
|
- Rejected: unnecessary persistence and duplication.
|
||||||
|
- Leave workspace/proof access unaudited.
|
||||||
|
- Rejected: the spec explicitly requires auditability for those consumption moments.
|
||||||
|
|
||||||
|
## Decision 9 — Keep browser coverage bounded to the existing smoke harness
|
||||||
|
|
||||||
|
**Decision**: Reuse [../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php](../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php) as the single browser smoke proof and expand only the existing `tests/Feature/Reviews/*` family for the rest.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The repo already has a browser harness for the review-detail-to-workspace handoff and calm disclosure checks.
|
||||||
|
- The rest of the productization contract is better proven in focused feature tests for omission rules, access states, and audit metadata.
|
||||||
|
- Adding a broader browser family would expand governance cost without clearer business proof.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add multiple new browser tests for each proof path.
|
||||||
|
- Rejected: too heavy for a bounded productization follow-up.
|
||||||
|
- Rely on browser testing alone.
|
||||||
|
- Rejected: feature tests remain the narrower proof for RBAC and disclosure-state coverage.
|
||||||
|
|
||||||
|
## Decision 10 — Keep the feature asset-light and non-search-expanding
|
||||||
|
|
||||||
|
**Decision**: Do not add a new asset bundle, new panel assets, or any new global-search scope as part of this follow-up.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The work is a content/disclosure/productization pass over existing Filament surfaces.
|
||||||
|
- Existing review, review-pack, and evidence resources already avoid global-search exposure in this customer-safe path.
|
||||||
|
- The user explicitly scoped out heavy asset strategy and new global-search work.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add new visual infrastructure or customer-safe asset loading.
|
||||||
|
- Rejected: no product need for this slice.
|
||||||
|
- Add a new search entry point for the customer workspace.
|
||||||
|
- Rejected: outside the tenant-safe reuse boundary and unnecessary for the current route.
|
||||||
348
specs/258-customer-review-productization/spec.md
Normal file
348
specs/258-customer-review-productization/spec.md
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
# Feature Specification: Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Feature Branch**: `258-customer-review-productization`
|
||||||
|
**Created**: 2026-04-30
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Customer Review Workspace Productization v1 as the smallest follow-up slice that hardens the existing customer review workspace into a calmer customer-safe review consumption surface with clearer findings, accepted-risk, evidence-summary, and auditable pack-access semantics without adding a portal, new persistence, or write paths."
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: TenantPilot already has repo-real review, evidence, review-pack, redaction, RBAC, and audit foundations plus an existing customer review workspace, but the current surface still reads more like an operator-led admin handoff than a fully customer-safe governance-of-record consumption path.
|
||||||
|
- **Today's failure**: Authorized customer reviewers, customer admins, and auditors can reach review truth, but they still have to infer meaning from operator-oriented wording, incomplete accepted-risk/accountability framing, uneven evidence proof semantics, and unclear access or absence states. That keeps the surface harder to sell and less trustworthy than the underlying repo truth.
|
||||||
|
- **User-visible improvement**: An authorized reader can open one existing workspace review surface and understand what was reviewed, what matters, what risk was accepted, what evidence exists, and what can be safely accessed or downloaded without seeing mutation paths, operator residue, or raw diagnostics.
|
||||||
|
- **Smallest enterprise-capable version**: Productize the existing customer review workspace and related released-review detail flow inside the current admin plane as a clearer read-only governance-of-record surface with calmer wording, findings and accepted-risk/accountability summaries, evidence proof pointers, explicit unavailable states, and auditable access/download semantics.
|
||||||
|
- **Explicit non-goals**: No new panel, no new provider registration path, no portal, no new identity plane, no new persistence, no review authoring or publishing engine, no remediation or mutation flow, no new review generation or regeneration flow, no AI summaries, no new global-searchable resource, no heavy new assets, and no destructive actions.
|
||||||
|
- **Permanent complexity imported**: One bounded productization pass over existing workspace and released-review surfaces, refined disclosure and access-state rules, explicit audit coverage for access/download behavior, focused feature-test expansion, and one bounded browser smoke check. No new models, enums, persisted artifacts, provider seams, or presentation frameworks are introduced.
|
||||||
|
- **Why now**: This remains the highest-priority active candidate in the roadmap overlay, and the implementation ledger still marks customer review productization as incomplete even though the underlying review foundations are already repo-real.
|
||||||
|
- **Why not local**: Isolated copy fixes or one-off detail-page tweaks would not establish a coherent customer-safe contract across workspace summary, released review detail, accepted-risk accountability, evidence proof pointers, and access/download semantics.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: Multi-surface disclosure contract and customer-facing wording inside an existing admin-plane surface. Defense: the slice stays read-only, derived, in-panel, and reuses current review/evidence/review-pack/RBAC/redaction/audit truth without adding new persistence or authoring behavior.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: canonical-view
|
||||||
|
- **Primary Routes**:
|
||||||
|
- existing admin-plane customer review workspace at `/admin/reviews/workspace`
|
||||||
|
- existing tenant-scoped released review detail route for the selected review
|
||||||
|
- existing review-pack access/download surface reached from released review context
|
||||||
|
- existing evidence summary/proof routes reached from released review context when the actor is entitled
|
||||||
|
- **Data Ownership**: All visible truth remains derived from existing tenant-owned review, finding, accepted-risk, evidence, review-pack, and audit records in the current workspace. No new workspace-owned or tenant-owned persisted artifact, projection table, or publication store is introduced.
|
||||||
|
- **RBAC**:
|
||||||
|
- this remains an admin-plane follow-up, not a new panel or plane
|
||||||
|
- workspace membership remains the first isolation boundary
|
||||||
|
- page entry requires an established workspace scope plus at least one entitled tenant the actor may read through the existing capability registry
|
||||||
|
- proof pointers, evidence summaries, and review-pack downloads remain capability-gated through current review/evidence/pack authorization paths
|
||||||
|
- non-members or out-of-scope tenant requests resolve as deny-as-not-found
|
||||||
|
- member actors lacking an optional gated capability receive capability denial only for the gated deep link or access path
|
||||||
|
- no new customer-only role family or identity model is introduced
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: When the workspace is launched from a tenant-scoped review or related review context, it prefilters to that tenant and foregrounds the latest released review for that tenant. Without an incoming tenant context, the page shows only entitled tenants in the current workspace.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Aggregated lists, review detail entry, proof pointers, and pack access only resolve for tenants the actor is entitled to in the current workspace. Inaccessible tenant targets are omitted from aggregated lists and resolve as not found when directly targeted.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes
|
||||||
|
- **Interaction class(es)**: status messaging, evidence/report viewers, action links, navigation entry points, access-state messaging, and review-pack access/download affordances
|
||||||
|
- **Systems touched**: existing `CustomerReviewWorkspace`, existing released review detail surfaces, existing review-pack access/download surface, existing evidence summary/proof presentation, existing redaction messaging, existing localization review copy, and existing audit infrastructure
|
||||||
|
- **Existing pattern(s) to extend**: the current customer review workspace, existing released review detail path, existing artifact-truth presentation, existing redaction notes, and current review-pack access semantics
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `SurfaceCompressionContext`, `ActionSurfaceDeclaration`, existing review/evidence/review-pack disclosure surfaces, and the current audit logging path
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: The underlying review, evidence, pack, and audit truth is already present and authoritative. What is insufficient is the current customer-safe product contract over that truth, not the underlying data model or access seams.
|
||||||
|
- **Allowed deviation and why**: none. This follow-up must tighten the existing shared path rather than introduce a second customer-review vocabulary or mirror presenter.
|
||||||
|
- **Consistency impact**: Review outcome wording, findings severity and status language, accepted-risk accountability phrasing, evidence proof terminology, pack availability states, redaction notes, and audit action labels must stay aligned across workspace and released-review detail flows.
|
||||||
|
- **Review focus**: Reviewers must block any new page-local taxonomy, raw-payload viewer, duplicate proof summary, or portal-only terminology that drifts from current review/evidence/review-pack truth.
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: `N/A`
|
||||||
|
- **Delegated start/completion UX behaviors**: `N/A`
|
||||||
|
- **Local surface-owned behavior that remains**: This slice stays read-only and does not add new run start, queue, resume, or completion behavior. Any existing deep provider or run diagnostics remain outside the default customer-safe path.
|
||||||
|
- **Queued DB-notification policy**: `N/A`
|
||||||
|
- **Terminal notification path**: `N/A`
|
||||||
|
- **Exception required?**: none
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
N/A - no shared provider/platform boundary touched. The slice consumes existing governance review truth and does not widen provider-specific semantics, identity scope, or shared platform taxonomy.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | yes | Native Filament page plus shared review/evidence primitives | status messaging, evidence/report viewers, access-state messaging, pack access | page, table/filter, disclosure state | no | Existing page is materially productized rather than replaced |
|
||||||
|
| Released Customer Review detail | yes | Native Filament resource/detail surface plus shared review/evidence primitives | status messaging, proof pointers, pack access, progressive disclosure | detail sections, disclosure state | no | Existing detail flow becomes the secondary customer-safe context surface |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | Primary Decision Surface | Customer reviewer, customer admin, or auditor decides whether the released review answers the current governance question or needs a follow-up conversation with the workspace operator team | released review state, calm outcome summary, key findings summary, accepted-risk/accountability summary, evidence/proof availability, and pack access state | released review detail, review-pack metadata, and proof pointers only after explicit open | Primary because it is the first truthful customer-safe route and should answer the high-level question without requiring a detail drilldown | Follows review-consumption workflow, not storage-object or operator-workbench navigation | Replaces cross-surface reconstruction with one calm starting point |
|
||||||
|
| Released Customer Review detail | Secondary Context Surface | Reader inspects the chosen released review to understand why the current governance record looks the way it does and what proof or packaged artifact is available | narrative outcome summary, findings summary, accepted-risk/accountability explanation, evidence summary, proof pointers, and pack availability | redacted proof references, release history, and gated secondary diagnostics only after explicit expansion | Not primary because it is entered from the workspace summary and should deepen understanding rather than replace the decision-first landing surface | Keeps the operator journey centered on one review case after the workspace summary selects it | Preserves a focused second step instead of exposing every proof or diagnostic on the first page |
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | customer-read-only, customer-admin, auditor-read-only, operator-MSP | what was reviewed, current outcome, key findings, accepted risks, evidence/proof availability, pack availability, and clear access/unavailable states | release timing, review lineage, and secondary freshness detail only after opening the review | raw payloads, provider IDs, run internals, and unrestricted diagnostics stay out of the default path | `Open released review` | raw/support detail, deep diagnostics, and unavailable secondary access paths stay hidden until explicitly requested and entitled | Workspace summary states each tenant review truth once; the detail surface elaborates instead of restating the same overview blocks |
|
||||||
|
| Released Customer Review detail | customer-read-only, customer-admin, auditor-read-only, operator-MSP | calm narrative outcome, findings summary, accepted-risk/accountability context, evidence summary with proof pointers, pack access state, and explicit redaction/access explanations | release history, evidence freshness detail, and secondary metadata only in collapsible or separately gated sections | raw evidence payloads, provider-debug detail, and unrestricted audit internals remain hidden or capability-gated | `Download current review pack` | raw/support detail, broad diagnostics, and any operator-only sections remain excluded from the customer-safe default view | Workspace page provides the overview; detail adds explanation and proof pointers without duplicating full summary cards at equal priority |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | List / Table / Read-only workspace report | Read-only registry report | Open the released review for the selected tenant | full-row open to released review detail | required | one safe inline pack-access affordance only when already available and entitled | none | `/admin/reviews/workspace` | `/admin/t/{tenant}/reviews/{record}` | workspace context, tenant prefilter, release state, pack availability | Customer review | what was reviewed, the released outcome, key findings, accepted-risk/accountability, proof availability, and access state | none |
|
||||||
|
| Released Customer Review detail | Detail / Report / Evidence | Read-only detail report | Download the current review pack or inspect proof pointers | sectioned detail page with one dominant safe header action | forbidden | proof pointers live in secondary evidence sections and capability-gated safe surfaces after the pack action | none | `/admin/reviews/workspace` | `/admin/t/{tenant}/reviews/{record}` | workspace, tenant, review release state, evidence summary, pack availability | Customer review | why this is the current governance record and what proof or packaged artifact is available | none |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | Customer reviewer, customer admin, or auditor with read access | Decide whether the released review is sufficient for current governance consumption or whether human follow-up is needed | Read-only workspace review overview | What was reviewed for my tenant, what matters now, what was accepted, and what can I safely open or download? | released outcome, key findings, accepted-risk/accountability summary, evidence/proof availability, pack access state, and explicit absence/unavailable messaging | release lineage, deeper evidence freshness, and secondary access metadata only after drilldown | review outcome, findings severity mix, accepted-risk lifecycle, evidence completeness, pack availability, access state | none | Open released review | none |
|
||||||
|
| Released Customer Review detail | Customer reviewer, customer admin, or auditor with read access | Understand the current governance record and consume proof or packaged artifacts safely | Read-only detail report | Why does the review say this, what evidence supports it, and what packaged artifact is available? | calm narrative summary, findings and recommendations, accepted-risk/accountability context, evidence summary and proof pointers, pack access state | release history, deeper proof metadata, and gated secondary diagnostics | review outcome, evidence freshness/completeness, accepted-risk timing, pack availability, redaction/access state | none | Download current review pack | none |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: no
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: no
|
||||||
|
- **New enum/state/reason family?**: no
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no
|
||||||
|
- **Current operator problem**: The repo already holds released review truth, but the current surface still makes customer-safe governance consumption harder than it should be by leaving too much operator framing and too little explicit accountability/proof language in the default path.
|
||||||
|
- **Existing structure is insufficient because**: The current workspace and detail contract does not yet consistently separate customer-readable review summary from secondary diagnostics, nor does it make access, absence, and unavailable states explicit enough for a sellable governance-of-record surface.
|
||||||
|
- **Narrowest correct implementation**: Tighten the existing workspace and released-review detail surfaces, reusing current review/evidence/review-pack/redaction/RBAC/audit truth and adding no new persistence, panel, or workflow engine.
|
||||||
|
- **Ownership cost**: A bounded copy and disclosure pass, focused audit assertions, targeted feature-test expansion, and one bounded browser smoke.
|
||||||
|
- **Alternative intentionally rejected**: A separate portal, customer-specific persistence layer, or new review publication framework was rejected because the repo already has the necessary read-only review truth and access seams.
|
||||||
|
- **Release truth**: current-release sellability blocker, not future-release preparation
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
|
||||||
|
|
||||||
|
Canonical replacement is preferred over preservation.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Feature, Browser
|
||||||
|
- **Validation lane(s)**: confidence, browser
|
||||||
|
- **Why this classification and these lanes are sufficient**: Focused feature tests are the narrowest sufficient proof for entitlement boundaries, progressive disclosure, explicit absence/access states, localization-ready copy expectations, and auditable access/download semantics. One bounded browser smoke remains justified to prove the calm default-visible path and the one-dominant-action flow under real UI rendering.
|
||||||
|
- **New or expanded test families**: expand the existing `Reviews/CustomerReviewWorkspace` feature family; keep exactly one bounded browser smoke around the same surface
|
||||||
|
- **Fixture / helper cost impact**: low to moderate. Reuse existing workspace membership, tenant entitlement, released review, findings, accepted-risk/exception, evidence snapshot, review pack, redaction, and audit fixtures instead of adding new provider or queue-heavy defaults.
|
||||||
|
- **Heavy-family visibility / justification**: exactly one browser smoke stays explicit because this slice is mostly about customer-safe wording, disclosure, and safe action placement. No broader browser family is introduced.
|
||||||
|
- **Special surface test profile**: shared-detail-family
|
||||||
|
- **Standard-native relief or required special coverage**: ordinary Filament feature coverage is sufficient for routing, authorization, disclosure, and unavailable states; the existing bounded smoke is the only required browser proof.
|
||||||
|
- **Reviewer handoff**: Reviewers must confirm that the customer-safe default view never exposes operator-only or mutation actions, that unauthorized tenant targets do not leak presence, that access/download events stay auditable, that review/evidence/pack absence states are explicit, and that no new global search leakage is introduced.
|
||||||
|
- **Budget / baseline / trend impact**: low feature-local increase only
|
||||||
|
- **Escalation needed**: none
|
||||||
|
- **Active feature PR close-out entry**: Smoke Coverage
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
- productize the existing admin-plane customer review workspace into a clearer customer-safe read-only governance-of-record surface
|
||||||
|
- tighten the released-review detail flow so it remains a customer-safe secondary context surface rather than an operator-heavy detail page
|
||||||
|
- findings summary in calm customer-safe language, including severity, status, impact, and recommendation
|
||||||
|
- accepted-risk/accountability summary using existing risk-acceptance truth, including decision reason, accountable person or role when available, timing, and expiry or re-review state
|
||||||
|
- evidence summary and proof pointers using existing evidence truth without raw payload default visibility
|
||||||
|
- explicit access, absence, unavailable, expired, and redaction states across workspace, review, evidence, and pack access
|
||||||
|
- auditable workspace access, review access, evidence-summary/proof access, and review-pack download behavior
|
||||||
|
- progressive disclosure boundaries that keep the default path calm and customer-readable
|
||||||
|
- reuse of existing localization, redaction, RBAC, audit, review-pack, and evidence truth
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- a new panel, portal, standalone customer shell, or separate identity plane
|
||||||
|
- new persistence, new publication state families, or a customer-specific projection store
|
||||||
|
- review authoring, review publishing, review generation, pack generation/regeneration, remediation, or other write paths
|
||||||
|
- risk acceptance editing, finding triage, owner reassignment, or admin mutation flows
|
||||||
|
- AI-generated review summaries or AI-generated recommendation layers
|
||||||
|
- raw evidence payload viewers, provider-debug views, or new operator diagnostics in the customer-safe default path
|
||||||
|
- a new global-searchable review or evidence resource
|
||||||
|
- broader baseline/control overlays or cross-surface "next sensible step" orchestration beyond the existing released-review contract
|
||||||
|
- new heavy frontend assets or a standalone asset-loading strategy
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- existing `CustomerReviewWorkspace` route and navigation entry point
|
||||||
|
- existing released `TenantReview` detail surface and release-truth semantics
|
||||||
|
- existing `ReviewPack` access and download behavior
|
||||||
|
- existing `EvidenceSnapshot` summaries and proof context
|
||||||
|
- existing finding and accepted-risk/exception truth
|
||||||
|
- existing redaction behavior and safe review-pack disclosure
|
||||||
|
- existing workspace and tenant RBAC plus entitlement enforcement
|
||||||
|
- existing audit logging for access and artifact events
|
||||||
|
- existing DE/EN localization posture for review-facing copy
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- The existing Filament v5 and Livewire v4 admin-plane customer review workspace remains the canonical entry surface for v1; no new panel, portal shell, or provider registration change is required.
|
||||||
|
- Released review truth already exists and remains authoritative for what is customer-safe to consume.
|
||||||
|
- Accepted-risk/accountability summaries can be derived from existing risk-acceptance or exception truth without inventing a new customer-facing decision store.
|
||||||
|
- Review packs remain the primary packaged export/proof artifact, and this slice only clarifies access and wording around them.
|
||||||
|
- Existing panel assets are sufficient; this slice does not justify heavy new asset registration or on-demand asset infrastructure.
|
||||||
|
- Existing tenant-safe search behavior remains unchanged; this slice does not depend on introducing a new global search surface.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Some released reviews may not yet carry fully populated accountable-person or accountable-role data, which could force partial accountability summaries until the underlying product truth is present.
|
||||||
|
- If productization changes only the workspace page and not the released-review detail follow-through, the customer-safe contract could still feel inconsistent after drilldown.
|
||||||
|
- Over-eager implementation could try to smuggle in publication, regeneration, or remediation behavior because the surrounding review foundations already exist; this spec must block that scope growth.
|
||||||
|
- Access auditing for read-only views can be under-specified if the implementation focuses only on download events; the slice must treat access and download behavior as separate auditable moments.
|
||||||
|
- If access, absence, and unavailable states are not differentiated clearly, users may misread missing proof as a system error or hidden operator state rather than a truthful product condition.
|
||||||
|
|
||||||
|
## Candidate Selection Rationale
|
||||||
|
|
||||||
|
- **Selected candidate**: Customer Review Workspace Productization v1
|
||||||
|
- **Source locations**:
|
||||||
|
- `docs/product/spec-candidates.md` active P0 candidate
|
||||||
|
- `docs/product/roadmap.md` priority order item 1
|
||||||
|
- `docs/product/implementation-ledger.md` open gap `Customer review productization remains incomplete`
|
||||||
|
- `specs/249-customer-review-workspace/spec.md` as the immediate predecessor spec
|
||||||
|
- existing repo surface and tests centered on the current customer review workspace
|
||||||
|
- **Why selected**: This is the highest-priority active unspecced follow-up that converts already repo-real review foundations into a more sellable customer-safe product surface without reopening foundations or requiring new persistence.
|
||||||
|
- **Why this is the smallest viable implementation slice**: The repo already has the workspace page, review detail, evidence, review-pack, redaction, RBAC, and audit truth. The missing piece is productization of wording, accountability summary, proof framing, and access-state semantics, not a new review engine.
|
||||||
|
- **Intentional narrowing from source candidate**: This slice deliberately defers broader baseline/control context overlays and richer cross-surface next-step guidance from the roadmap candidate. Those remain follow-up work for `Compliance Evidence Mapping v1` and `Governance-as-a-Service Packaging v1` after the customer-safe review contract is stable.
|
||||||
|
- **Why close alternatives are deferred**:
|
||||||
|
- Governance Decision Surface Convergence already has `specs/257-governance-decision-convergence` and addresses a different operator convergence lane.
|
||||||
|
- Remove Findings Lifecycle Backfill Runtime Surfaces already has `specs/253-remove-findings-backfill-runtime-surfaces` and is a cleanup lane, not the current customer-safe sellability blocker.
|
||||||
|
- Remove Legacy Acknowledged Finding Status Compatibility already has `specs/254-remove-acknowledged-compat` and is a workflow semantics cleanup lane.
|
||||||
|
- Enforce Creation-Time Finding Invariants already has `specs/255-enforce-finding-creation-invariants` and is a data-integrity hardening lane.
|
||||||
|
- Cross-Tenant Compare and Promotion v1 already has `specs/043-cross-tenant-compare-and-promotion` and remains a separate refresh track rather than the next unspecced customer-review productization slice.
|
||||||
|
|
||||||
|
## Follow-up Candidates
|
||||||
|
|
||||||
|
- Governance-as-a-Service Packaging v1 once released reviews, proof pointers, and accepted-risk summaries are stable enough to package as a repeatable management deliverable
|
||||||
|
- Compliance Evidence Mapping v1 once the customer-safe review surface needs a stronger control/readiness and baseline/control-context overlay than this released-review productization slice intentionally provides
|
||||||
|
- Cross-Tenant Compare and Promotion v1 as the next MSP multiplier after customer-safe review consumption is calmer
|
||||||
|
- Broader risk acceptance and accountability reporting follow-through if customer-safe accountability views need portfolio or management-level rollups beyond the review surface
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Understand the latest released review at a glance (Priority: P1)
|
||||||
|
|
||||||
|
A customer reviewer, customer admin, or auditor wants one calm workspace route that shows the latest released review for each entitled tenant so they can understand the current governance record without reconstructing it from operator-oriented screens.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core sellability gap. If the reader still has to search across internal review, evidence, and pack surfaces to understand the current state, the productization slice fails.
|
||||||
|
|
||||||
|
**Independent Test**: Sign in as an entitled read-only actor, open the customer review workspace, and confirm that each visible tenant shows a released review summary with findings, accepted-risk/accountability, evidence/proof, and pack availability in customer-safe wording.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an entitled actor has access to one or more tenants with released reviews, **When** they open the customer review workspace, **Then** they see only entitled tenants and only released review summaries in the default path.
|
||||||
|
2. **Given** a tenant has released findings, accepted risks, and proof-backed evidence, **When** the actor scans the workspace row or card, **Then** they can understand what was reviewed, what matters, and what proof or pack is available without opening a second page first.
|
||||||
|
3. **Given** the actor has no entitled tenant with a released review, **When** they open the workspace, **Then** they see a truthful absence state rather than leaked draft or hidden internal review states.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Understand why the review says what it says (Priority: P1)
|
||||||
|
|
||||||
|
A customer reviewer, customer admin, or auditor wants the released review detail to explain findings, accepted-risk/accountability, and evidence proof in calmer customer-safe wording so they can trust the review as the governance record without seeing admin or debug residue.
|
||||||
|
|
||||||
|
**Why this priority**: Productization is not complete if the workspace summary is calmer but the first drilldown still feels like an operator surface.
|
||||||
|
|
||||||
|
**Independent Test**: Open a released review from the workspace and verify that the default detail view shows summary, findings, accepted-risk/accountability, and evidence proof pointers while hiding mutation actions and raw diagnostics.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a tenant has a released review with findings and accepted risks, **When** the actor opens the released review detail, **Then** the page explains the outcome, recommendations, accepted-risk accountability, and proof pointers in customer-safe language.
|
||||||
|
2. **Given** deeper evidence or proof metadata exists, **When** the actor stays on the default detail view, **Then** raw payloads, provider-debug context, and broad diagnostics remain hidden until explicitly requested and entitled.
|
||||||
|
3. **Given** the actor lacks an optional capability for a secondary proof or pack action, **When** they view the released review detail, **Then** the review still remains readable while the gated access path is explicitly unavailable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Safely consume packaged proof and understand unavailable states (Priority: P2)
|
||||||
|
|
||||||
|
A customer reviewer, customer admin, or auditor wants to access the current review pack or understand why it is unavailable so they can consume the released artifact without operator assistance or confusion.
|
||||||
|
|
||||||
|
**Why this priority**: The review workspace is more trustworthy when access and absence states are explicit instead of silent or operator-only.
|
||||||
|
|
||||||
|
**Independent Test**: Open the workspace and released review detail for tenants with and without available packs or proof access, then verify explicit access, unavailable, expired, or redacted states plus auditable access/download behavior.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a tenant has a current review pack and the actor is entitled to access it, **When** they choose the pack action, **Then** they can access or download the existing artifact without triggering any generation or mutation flow.
|
||||||
|
2. **Given** a tenant lacks a current review pack or proof access, **When** the actor views the workspace or released review detail, **Then** the surface shows a truthful unavailable state instead of implying a hidden operator path.
|
||||||
|
3. **Given** the actor tries to target a tenant outside their scope, **When** they open the workspace with that tenant context or a direct review link, **Then** the system resolves as not found and reveals no review or pack presence.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when a tenant has findings and accepted risks but no released review yet? The customer-safe workspace shows an explicit `No released review available yet` style state rather than leaking internal review lifecycle.
|
||||||
|
- What happens when a released review exists but accountability data is only partially populated? The summary shows only confirmed accountability truth and does not invent a placeholder owner or decision role.
|
||||||
|
- What happens when a review pack exists but access is unavailable because of entitlement or redaction posture? The UI shows a clear access or unavailable state and does not offer a generation or recovery action.
|
||||||
|
- What happens when a user enters through a saved filter or tenant-prefilter for an inaccessible tenant? The filter resolves safely without exposing the tenant or its review artifacts.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no write/change behavior, no new queue or scheduled behavior, and no new persistence. It changes customer-safe disclosure, authorization boundaries on read-only surfaces, and audit expectations for access/download behavior.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This follow-up must stay derived. It must not introduce new persistence, new presentation frameworks, new customer state families, or speculative abstractions.
|
||||||
|
|
||||||
|
**Constitution alignment (XCUT-001):** The feature must extend existing review, evidence, review-pack, localization, redaction, and audit paths rather than invent a page-local customer-review semantic layer.
|
||||||
|
|
||||||
|
**Constitution alignment (DECIDE-AUD-001 / OPSURF-001):** The default path must separate customer-readable decision content from deeper diagnostics and keep raw/support detail hidden by default.
|
||||||
|
|
||||||
|
**Constitution alignment (TEST-GOV-001):** The implementation must stay with focused feature coverage plus one bounded browser smoke and avoid creating a broader heavy family.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** Workspace and tenant membership remain deny-as-not-found boundaries. Optional secondary disclosure remains capability-gated inside an established scope. No new role strings or raw capability strings are introduced by this spec.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001 / UI-NAMING-001 / DECIDE-001 / ACTSURF-001):** The slice must remain a native Filament read-only reporting flow with one dominant safe inspect action and no destructive or mutation actions.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: The system MUST keep this slice inside the existing admin-plane customer review workspace and released-review detail flow rather than creating a new panel, portal, or identity surface.
|
||||||
|
- **FR-002**: The system MUST derive every visible summary, access state, and proof affordance from existing review, evidence, review-pack, accepted-risk, redaction, entitlement, and audit truth without creating new persisted customer-review artifacts.
|
||||||
|
- **FR-003**: The default workspace path MUST show only entitled tenants and only released or otherwise customer-safe reviews as the governance-of-record path.
|
||||||
|
- **FR-004**: The default-visible workspace summary MUST answer, in calm customer-safe language, what was reviewed, the current outcome, key findings, accepted risks, available evidence or proof, and pack availability.
|
||||||
|
- **FR-005**: Findings shown through this slice MUST present severity, status, impact, and recommendation in customer-safe wording and MUST not depend on operator-only vocabulary to be understood.
|
||||||
|
- **FR-006**: Accepted-risk content shown through this slice MUST summarize decision reason, accountable person or role when product truth exists, decision timing, current expiry or re-review state, and linked proof context without exposing internal workflow residue.
|
||||||
|
- **FR-007**: Evidence shown through this slice MUST present a narrative summary and proof pointers and MUST not expose raw payloads, provider-debug data, or unrestricted diagnostics by default.
|
||||||
|
- **FR-008**: The workspace and released-review detail surfaces MUST make access, absence, expired, unavailable, and redaction states explicit and understandable without implying a hidden write path or missing internal admin permission.
|
||||||
|
- **FR-009**: The feature MUST use progressive disclosure so that default-visible content remains customer-readable while deeper review detail, evidence context, and any gated secondary diagnostics appear only after explicit user intent and capability checks.
|
||||||
|
- **FR-010**: The primary workspace action MUST be opening the released review, and any secondary safe proof or pack action MUST never compete with write, remediation, generation, or admin actions.
|
||||||
|
- **FR-011**: Review-pack access and downloads MUST reuse existing entitlement, redaction, and access rules and MUST never trigger generation, regeneration, publication, or any other mutation from this slice.
|
||||||
|
- **FR-012**: Every explicit workspace access, released-review access, evidence summary or proof access, and review-pack download exposed by this slice MUST remain auditable through the current audit infrastructure.
|
||||||
|
- **FR-013**: Non-members or out-of-scope workspace or tenant requests MUST resolve as deny-as-not-found, while actors inside an established scope MUST receive explicit capability denial only for gated secondary actions or deep links they are not allowed to use.
|
||||||
|
- **FR-014**: When launched from tenant-scoped context, the workspace MUST preserve a safe tenant prefilter and context return path without broadening discovery beyond entitled tenants.
|
||||||
|
- **FR-015**: The slice MUST NOT introduce a new global-searchable resource or broaden existing search discovery in a way that reveals review or evidence artifacts across tenant boundaries.
|
||||||
|
- **FR-016**: Any new or revised status, severity, availability, or redaction labels in this slice MUST stay aligned with existing centralized semantics rather than page-local mappings.
|
||||||
|
- **FR-017**: Customer-facing labels and guidance introduced by this slice MUST remain localization-ready for the existing DE/EN product language posture.
|
||||||
|
- **FR-018**: The slice MUST expose no destructive, remediation, authoring, publishing, generation, or admin-only actions.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace | existing `App\Filament\Pages\Reviews\CustomerReviewWorkspace` | `Clear filters` only | `recordUrl()` or full-row open to the released review | `Open released review`, `Download current review pack` when already available and entitled | none | `Clear filters` only when filters are active; otherwise explanatory no-data text with no create CTA | `N/A` | `N/A` | yes | Action Surface Contract satisfied: exactly one primary inspect model, no redundant view action, no empty action groups, no destructive actions |
|
||||||
|
| Released Customer Review detail | existing tenant-scoped released review detail surface | none by default | `N/A` | `N/A` | none | `N/A` | `Download current review pack` only; evidence summary remains in secondary in-body proof sections when entitled | `N/A` | yes | Customer-safe launch mode hides publish, refresh, generate, archive, remediation, and other operator-only actions while keeping one dominant safe header action |
|
||||||
|
|
||||||
|
Action Surface Contract is satisfied for this slice. Each affected surface keeps one primary inspect/open model, no empty `ActionGroup` or `BulkActionGroup` placeholder, and no destructive-action placement rules are needed because destructive actions are out of scope. `UI-FIL-001` and `UX-001` are satisfied by staying inside native Filament read-only surfaces, using explicit empty states, and keeping status emphasis aligned to shared review semantics rather than page-local visual language.
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Customer Review Workspace Summary**: A derived workspace-scoped summary for one entitled tenant that combines the current released review, findings overview, accepted-risk/accountability summary, evidence proof availability, and pack access state without becoming a persisted entity.
|
||||||
|
- **TenantReview**: The existing released review artifact that anchors what was reviewed, the current governance outcome, and the released detail path.
|
||||||
|
- **Finding**: The existing issue-level governance truth that feeds the customer-safe findings summary and recommendation framing.
|
||||||
|
- **Accepted Risk Decision**: The existing accepted-risk or exception truth that explains why a risk was accepted, who is accountable when product truth exists, and when the decision should be revisited.
|
||||||
|
- **EvidenceSnapshot**: The existing supporting proof artifact that informs evidence summaries and proof pointers.
|
||||||
|
- **ReviewPack**: The existing packaged review artifact that remains the primary downloadable proof bundle.
|
||||||
|
- **AuditLog**: The existing audit trail used to record explicit access and download behavior without introducing a new audit store.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: An entitled read-only actor can answer what was reviewed, what matters, what was accepted, what evidence exists, and what artifact is available from the customer review workspace in two interactions or fewer.
|
||||||
|
- **SC-002**: In 100% of validated customer-safe scenarios, the default-visible workspace and released-review detail path shows no mutation, remediation, publication, or operator-debug actions.
|
||||||
|
- **SC-003**: In 100% of validated unauthorized workspace or tenant access scenarios, the feature reveals no cross-tenant review, evidence, or pack presence.
|
||||||
|
- **SC-004**: For tenants with a released review and available pack, entitled users can open the released review or access the pack on their first attempt without operator assistance.
|
||||||
|
- **SC-005**: For tenants without released review truth, proof access, or a current pack, the surface explains the absence or unavailability explicitly rather than showing a blank state or generic error.
|
||||||
205
specs/258-customer-review-productization/tasks.md
Normal file
205
specs/258-customer-review-productization/tasks.md
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
description: "Task list for Customer Review Workspace Productization v1"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Customer Review Workspace Productization v1
|
||||||
|
|
||||||
|
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/`
|
||||||
|
**Prerequisites**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/plan.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/spec.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/checklists/requirements.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/contracts/customer-review-productization.openapi.yaml`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/quickstart.md`
|
||||||
|
|
||||||
|
**Tests**: Required (Pest) for runtime behavior changes. Keep proof in the narrow `confidence` lane plus one bounded `browser` smoke only because this slice changes customer-safe wording, disclosure order, and safe action hierarchy on existing workspace and detail surfaces.
|
||||||
|
**Operations**: No new `OperationRun`, queue, remote call, publication flow, remediation flow, or background processing is introduced. Auditability stays on the current shared audit pipeline only.
|
||||||
|
**RBAC**: Workspace membership remains the first boundary. Non-members or out-of-scope tenant targets remain `404`; in-scope actors missing an optional capability get explicit unavailability or `403` only on the gated secondary path. Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Auth/RoleCapabilityMap.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Auth/Capabilities.php`; do not add raw capability strings or role-string checks.
|
||||||
|
**Shared Pattern Reuse**: Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/SurfaceCompressionContext.php`, existing review-pack and evidence resources, and the shared audit logger rather than creating a new customer shell, presenter family, or persistence layer.
|
||||||
|
**Organization**: Tasks are grouped by user story so workspace productization, released-review detail hardening, and packaged-proof access remain independently testable after shared seams are settled.
|
||||||
|
|
||||||
|
## Test Governance Notes
|
||||||
|
|
||||||
|
- Lane assignment: `confidence` plus one explicit `browser` smoke remain the narrowest sufficient proof for capability-first RBAC, workspace and tenant isolation, calmer disclosure hierarchy, explicit unavailable states, and auditable pack or proof consumption.
|
||||||
|
- Keep new coverage inside `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspace*.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`; do not widen this slice into a new browser family or a broader cross-tenant workboard suite.
|
||||||
|
- Reuse existing workspace membership, entitled-tenant, released review, finding exception, evidence snapshot, review pack, localization, and audit fixtures; any helper added during implementation must stay explicit and cheap by default.
|
||||||
|
- If implementation finds that workspace-open or proof-open auditing is already fully covered, close the corresponding audit tasks as reuse-only and record the outcome as `document-in-feature` instead of adding new action IDs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Lock the bounded productization delta, current proof lanes, and exact repo anchors before runtime edits begin.
|
||||||
|
|
||||||
|
- [x] T001 Review the bounded slice, non-goals, guardrail outcomes, and required customer-safe behaviors in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/spec.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/plan.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/checklists/requirements.md`
|
||||||
|
- [x] T002 [P] Review route reuse, derived disclosure states, audit expectations, and no-new-persistence constraints in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/data-model.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/contracts/customer-review-productization.openapi.yaml`
|
||||||
|
- [x] T003 [P] Confirm the focused Sail/Pest commands, the single bounded browser-smoke requirement, and the existing review test family in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/quickstart.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Settle the shared RBAC, isolation, audit, presenter, and localization seams that every user story depends on.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||||
|
|
||||||
|
- [x] T004 [P] Add shared authorization coverage for workspace membership, entitled-tenant omission, deny-as-not-found tenant targeting, and in-scope optional-capability denial in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php`
|
||||||
|
- [x] T005 Reuse or minimally tighten capability-first workspace and tenant isolation in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Auth/Capabilities.php`, and existing capability maps so later summary, proof, and pack work stays on one existing seam
|
||||||
|
- [x] T006 [P] Verify and extend the shared audit pipeline only where required in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Audit/AuditActionId.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` for workspace entry, review open, proof open, and pack download moments
|
||||||
|
- [x] T007 [P] Confirm the shared customer-safe truth presentation seams to reuse in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/SurfaceCompressionContext.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` before story-specific disclosure changes begin
|
||||||
|
- [x] T008 [P] Inventory the bounded copy, localization, and tenant-safe global-search seams for calmer wording, customer-safe disclosure hierarchy, explicit access-state labels, and unchanged review/evidence discoverability in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`
|
||||||
|
|
||||||
|
**Checkpoint**: Shared RBAC, isolation, audit, presenter, and localization seams are fixed before workspace or detail productization begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Understand The Latest Released Review At A Glance (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Let an entitled customer reviewer, customer admin, or auditor open one existing workspace route and immediately understand the current released governance record per entitled tenant.
|
||||||
|
|
||||||
|
**Independent Test**: Open `/admin/reviews/workspace` as an entitled actor and confirm each visible tenant shows only released review truth, customer-safe summary wording, one dominant `Open released review` action, and explicit absence or unavailable states without leaking internal review lifecycle.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [x] T009 [P] [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` for latest released review-only rows, calmer at-a-glance summaries, accepted-risk accountability visibility, evidence or pack availability summaries, and truthful no-released-review states
|
||||||
|
- [x] T010 [P] [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` for launches into `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` from existing review, evidence, and review-pack detail paths with safe tenant prefilter and no broadened tenant discovery
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [x] T011 [US1] Compose one workspace entry per entitled tenant from released review truth only in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` and the existing tenant-review register query seam, reusing existing `TenantReview`, current pack, and evidence relationships without draft or internal fallback
|
||||||
|
- [x] T012 [US1] Reuse or minimally tighten launch and prefilter handoffs in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` so review, evidence, and review-pack detail paths enter the workspace with preserved tenant context and no new shell
|
||||||
|
- [x] T013 [US1] Rework the default-visible workspace disclosure hierarchy in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` so outcome, key findings, accepted-risk accountability, evidence availability, and review-pack state appear as decision-first content with exactly one dominant `Open released review` action
|
||||||
|
- [x] T014 [US1] Implement explicit workspace absence, unavailable, partial, expired, and redaction-safe messaging in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` without introducing a new persisted state family
|
||||||
|
- [x] T015 [US1] Update calmer workspace wording and DE or EN localization keys in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` so operator-led language is removed from the customer-safe entry surface
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 1 is independently functional when the workspace truthfully shows only released review summaries for entitled tenants with clear customer-safe wording and explicit access-state handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Understand Why The Review Says What It Says (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Let the same actor open the released review detail from the workspace and understand findings, accepted-risk accountability, and proof context without seeing operator or mutation residue.
|
||||||
|
|
||||||
|
**Independent Test**: Open a released review from the workspace and confirm the detail stays read-only, keeps customer-safe disclosure first, preserves tenant and workspace context, and makes optional proof or pack unavailability explicit instead of hiding or leaking content.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [x] T016 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php` for workspace-to-review handoff, preserved tenant context, safe return semantics, and deny-as-not-found behavior when `customer_workspace=1` targets an out-of-scope tenant or review
|
||||||
|
- [x] T017 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php` for customer-workspace read-only mode, one dominant safe action, hidden publish or remediation controls, explicit unavailable states for optional secondary actions, and preserved tenant-safe global-search posture across the touched review/evidence/pack resources
|
||||||
|
- [x] T018 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php` for calmer findings language, accepted-risk accountability framing, evidence summary ordering, and hidden raw or support detail by default when the detail is launched from the workspace
|
||||||
|
- [x] T019 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` for the calm workspace-to-detail handoff, one dominant primary action, and truthful optional-action unavailable states while keeping browser proof bounded to this single smoke slice
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [x] T020 [US2] Tighten the existing customer-workspace detail mode in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php` so the released review remains read-only, capability-aware, and context-preserving without creating a second detail surface
|
||||||
|
- [x] T021 [US2] Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Ui/GovernanceArtifactTruth/SurfaceCompressionContext.php`, and the current review and finding-exception relationships to present calmer findings, accepted-risk accountability, and evidence-summary disclosure on the existing released-review detail surface
|
||||||
|
- [x] T022 [US2] Reuse the related detail surfaces in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so secondary pack and proof paths preserve customer-safe wording, source context, and explicit unavailable messaging after drilldown
|
||||||
|
- [x] T023 [US2] Update customer-safe detail, pack, and proof copy in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` so calmer wording and the customer-safe disclosure hierarchy stay aligned across the flow
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 2 is independently functional when the released review detail deepens understanding without exposing operator controls, duplicate summaries, or raw support detail by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Safely Consume Packaged Proof And Understand Unavailable States (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Let the actor access the current review pack or proof route when entitled, or understand exactly why it is unavailable, expired, absent, or redacted.
|
||||||
|
|
||||||
|
**Independent Test**: From the workspace and released-review detail, verify that current review-pack download and proof routes stay capability-gated, explicit when unavailable, auditable, and never trigger generation, regeneration, publication, or remediation behavior.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [x] T024 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` for available, unavailable, expired, absent, and redacted pack or proof states plus explicit customer-safe messaging on the workspace and released-review detail surfaces
|
||||||
|
- [x] T025 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php` for signed current-pack download reuse, `source_surface=customer_review_workspace` audit metadata, and the absence of generate, regenerate, or publication behavior on this path
|
||||||
|
- [x] T026 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php` for proof-route capability gating, explicit unavailable or redacted handling, and shared audit logging when proof is opened from the customer review flow
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [x] T027 [US3] Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php` so current-pack access stays capability-first, signed, auditable, and explicit when absent, unavailable, expired, or redacted
|
||||||
|
- [x] T028 [US3] Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so proof pointers remain customer-safe, capability-gated, and explicit when proof is absent, unavailable, or redacted instead of silently omitted
|
||||||
|
- [x] T029 [US3] Finalize shared audit wiring in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Audit/AuditActionId.php` for workspace entry, review open, proof open, and pack download only where T006 confirmed a real gap, preserving stable tenant, workspace, and `source_surface` metadata on the existing pipeline
|
||||||
|
- [x] T030 [US3] Align pack and proof access wording in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` so access, absence, unavailable, expired, and redacted states stay distinct and customer-safe
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 3 is independently functional when current pack and proof access remain bounded, auditable, and explicit under all supported access states.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Run the narrow validation set, keep formatting clean, and record bounded reviewer outcomes without widening scope.
|
||||||
|
|
||||||
|
- [x] T031 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- [x] T032 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- [x] T033 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- [x] T034 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- [x] T035 Record the final `Guardrail / Smoke Coverage` close-out, lane results, audit-gap outcome (`reuse-only` vs bounded additive action IDs), localization or copy scope outcome, global-search safety outcome, and any `document-in-feature` or `follow-up-spec` decision in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/plan.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/quickstart.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/258-customer-review-productization/checklists/requirements.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||||
|
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user stories until RBAC, isolation, audit, presenter, and localization seams are settled.
|
||||||
|
- **Phase 3 (US1)**: depends on Phase 2 and delivers the MVP workspace productization slice.
|
||||||
|
- **Phase 4 (US2)**: depends on Phase 2 and should follow US1 because it deepens the same review-consumption flow on shared surfaces.
|
||||||
|
- **Phase 5 (US3)**: depends on Phase 2 and is safest after US1 or US2 because pack and proof actions build on the same workspace and detail context.
|
||||||
|
- **Phase 6 (Polish)**: depends on all implemented stories.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: first independently shippable increment once Phase 2 is complete.
|
||||||
|
- **US2 (P1)**: independently testable after Phase 2, but should merge after US1 because the same workspace and detail surfaces are shared hotspots.
|
||||||
|
- **US3 (P2)**: independently testable after Phase 2, but should merge after US1 because explicit access-state handling depends on the final workspace and detail wording contract.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Write the listed Pest coverage first and make it fail for the intended gap before runtime implementation.
|
||||||
|
- Reuse shared RBAC, audit, presenter, and localization seams before introducing any local helper or new copy mapping.
|
||||||
|
- Re-run the narrowest relevant proof command after each story checkpoint before moving to the next story.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- T002 and T003 can run in parallel after T001 confirms the bounded slice.
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- T004, T006, T007, and T008 can run in parallel while T005 settles the shared capability-first control path.
|
||||||
|
|
||||||
|
### User Story 1
|
||||||
|
|
||||||
|
- T009 and T010 can run in parallel before runtime edits begin.
|
||||||
|
- After T011 settles row composition, T012 can proceed before T013 through T015 finalize disclosure, states, and copy.
|
||||||
|
|
||||||
|
### User Story 2
|
||||||
|
|
||||||
|
- T016, T017, T018, and T019 can run in parallel because they cover different proof surfaces in the same flow.
|
||||||
|
- After the tests exist, T020 through T023 should land in order because they touch the same released-review detail family.
|
||||||
|
|
||||||
|
### User Story 3
|
||||||
|
|
||||||
|
- T024, T025, and T026 can run in parallel.
|
||||||
|
- After the tests exist, T027 and T028 can proceed in parallel before T029 and T030 finalize audit and wording alignment.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Suggested MVP Scope
|
||||||
|
|
||||||
|
- MVP = **Phase 2 + User Story 1** only. That delivers the calmer customer-safe workspace entry surface, capability-first tenant isolation, explicit access-state handling, and safe launch continuity without yet deepening the released-review detail surface.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Complete Phase 1 and Phase 2.
|
||||||
|
2. Deliver US1 and validate the workspace productization contract.
|
||||||
|
3. Deliver US2 and validate the released-review detail follow-through.
|
||||||
|
4. Deliver US3 and validate packaged-proof access, unavailable states, and audit reuse.
|
||||||
|
5. Finish with Phase 6 validation, formatting, and reviewer close-out notes.
|
||||||
|
|
||||||
|
### Team Strategy
|
||||||
|
|
||||||
|
1. Settle the shared capability, audit, presenter, and localization seams first.
|
||||||
|
2. Parallelize test authoring inside each story before converging on the shared workspace and detail files.
|
||||||
|
3. Serialize merges around `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` because they are the primary conflict hotspots for this slice.
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
# Preparation Review Checklist: Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Purpose**: Validate repo-fit preparation quality after `spec.md`, `plan.md`, and `tasks.md` are complete
|
||||||
|
**Reviewed**: 2026-04-30
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
**Supporting artifacts**: [plan.md](../plan.md), [research.md](../research.md), [data-model.md](../data-model.md), [quickstart.md](../quickstart.md), [tasks.md](../tasks.md), [compliance-evidence-mapping.openapi.yaml](../contracts/compliance-evidence-mapping.openapi.yaml)
|
||||||
|
**Related standards**: [List Surface Review Checklist](../../../docs/product/standards/list-surface-review-checklist.md)
|
||||||
|
|
||||||
|
## Candidate Fit
|
||||||
|
|
||||||
|
- [x] The selected candidate still matches the active `Compliance Evidence Mapping v1` entry in `docs/product/spec-candidates.md`, the sequencing in `docs/product/roadmap.md`, and the moat blocker wording in `docs/product/implementation-ledger.md`
|
||||||
|
- [x] Existing `specs/` coverage was checked so this package stays a new follow-up rather than duplicating Specs 249 through 258
|
||||||
|
- [x] The scope stays on one bounded interpretation overlay over existing canonical-control and review truth instead of reopening control foundations or packaging work
|
||||||
|
- [x] Governance-as-a-Service Packaging and framework-specific overlays are explicitly deferred rather than hidden inside this slice
|
||||||
|
|
||||||
|
## Constitution Fit
|
||||||
|
|
||||||
|
- [x] The package stays on the existing Filament v5 plus Livewire v4 admin plane and does not introduce panel or provider-registration work beyond the current `bootstrap/providers.php` truth
|
||||||
|
- [x] No new persistence table, no new report engine, no OperationRun workflow, no portal shell, and no destructive action surface are introduced
|
||||||
|
- [x] Workspace and tenant isolation remain explicit, including `404` for non-members and out-of-scope tenant targets and capability gating only on reused secondary evidence paths
|
||||||
|
- [x] One dominant safe action per changed surface is explicitly described, with workspace list and detail disclosure roles remaining consistent across spec, plan, and tasks
|
||||||
|
- [x] Global-search safety is preserved without introducing a new searchable resource or widening review/evidence discovery across tenant boundaries
|
||||||
|
- [x] Asset strategy remains unchanged; if later implementation unexpectedly registers assets, deployment still uses the existing `cd apps/platform && php artisan filament:assets` step
|
||||||
|
|
||||||
|
## Surface Guardrails
|
||||||
|
|
||||||
|
- [x] The package references and satisfies the repo's [List Surface Review Checklist](../../../docs/product/standards/list-surface-review-checklist.md) for the customer review workspace list surface
|
||||||
|
- [x] The customer review workspace remains the primary decision surface with one dominant `Open released review` path and no competing list-row proof action
|
||||||
|
- [x] The released-review detail surface remains explanation-first, read-only in customer-workspace mode, and keeps supporting evidence as explicit in-body drilldown
|
||||||
|
- [x] No page-local control taxonomy, framework naming, or second interpretation path is introduced across the changed surfaces
|
||||||
|
|
||||||
|
## Artifact Consistency
|
||||||
|
|
||||||
|
- [x] `spec.md`, `plan.md`, `tasks.md`, `data-model.md`, and the conceptual contract all target the same shared `control_interpretation` contract and the same workspace plus released-review detail flow
|
||||||
|
- [x] The primary released-review detail route now follows the same `404` posture described in the spec, with explicit `403` handling reserved only for gated secondary evidence routes
|
||||||
|
- [x] The workspace contract now models only entitled tenants with a released review, while the no-released-review case remains a page-level empty state instead of a parallel row model
|
||||||
|
- [x] The required prep artifact `checklists/requirements.md` exists and includes explicit review outcome and workflow outcome fields
|
||||||
|
- [x] The required `.specify/scripts/bash/update-agent-context.sh copilot` step is recorded as completed during planning
|
||||||
|
|
||||||
|
## Test Governance
|
||||||
|
|
||||||
|
- [x] Validation lanes remain explicitly bounded to `confidence` plus one existing `browser` smoke
|
||||||
|
- [x] The package reuses existing `TenantReview`, `CustomerReviewWorkspace`, and evidence proof test families instead of creating a new heavy-governance or browser family
|
||||||
|
- [x] Reviewer proof commands remain explicit and minimal for the touched workspace, detail, evidence, and audit surfaces
|
||||||
|
- [x] The package includes explicit close-out handling for global-search safety, shared-interpretation-path consistency, and audit-metadata reuse
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reviewed after `spec.md`, `plan.md`, `research.md`, `data-model.md`, `quickstart.md`, `tasks.md`, and the conceptual contract were aligned on 2026-04-30.
|
||||||
|
- This repository's preparation artifacts are intentionally implementation-oriented, so concrete routes, classes, list-surface standards, and validation commands are expected rather than treated as leakage.
|
||||||
|
- Implementation completed on 2026-04-30. The implementation keeps one shared `control_interpretation` contract, reuses existing audit events, preserves global-search disablement, and keeps the customer review workspace list surface released-review-only with one dominant inspect action.
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
- **Outcome class**: `acceptable-special-case`
|
||||||
|
- **Outcome**: `keep`
|
||||||
|
- **Reason**: The package keeps the new semantic layer bounded to one versioned interpretation overlay, records the list-surface guardrail expectations, aligns primary-route access semantics to the repo's `404` posture, and removes the extra no-review row branch so the implementation target stays narrow.
|
||||||
|
- **Workflow result**: Implemented and validated after the Spec Kit implementation loop.
|
||||||
|
|
||||||
|
## Implementation Review Outcome
|
||||||
|
|
||||||
|
- **Guardrail / Smoke Coverage**: PASS. Focused feature/browser tests and adjacent contract tests passed; Pint passed.
|
||||||
|
- **Shared interpretation path**: PASS. Composition writes one stored v1 interpretation; workspace and detail read it.
|
||||||
|
- **Audit metadata reuse**: PASS. Existing events carry `source_surface`, `review_id` where applicable, `tenant_filter_id`, and `interpretation_version`; no new event family was introduced.
|
||||||
|
- **Global-search safety**: PASS. Tenant review, review pack, and evidence resources remain globally disabled.
|
||||||
|
- **Residual risks**: none confirmed in scope after the implementation loop.
|
||||||
@ -0,0 +1,292 @@
|
|||||||
|
openapi: 3.0.3
|
||||||
|
info:
|
||||||
|
title: TenantPilot Compliance Evidence Mapping v1 (Conceptual)
|
||||||
|
version: 0.1.0
|
||||||
|
description: |
|
||||||
|
Conceptual contract for the Compliance Evidence Mapping v1 planning package.
|
||||||
|
|
||||||
|
These paths describe existing Filament admin and tenant-scoped routes reused by
|
||||||
|
the implementation. The schemas document the shared interpretation contract the
|
||||||
|
feature is expected to add to existing review payloads; they do not define a new
|
||||||
|
public REST API.
|
||||||
|
servers:
|
||||||
|
- url: /
|
||||||
|
paths:
|
||||||
|
/admin/reviews/workspace:
|
||||||
|
get:
|
||||||
|
summary: View the compliance evidence mapping workspace
|
||||||
|
description: |
|
||||||
|
Existing admin-plane customer review workspace page reused as the primary
|
||||||
|
decision surface for mapped control readiness summaries. The route remains
|
||||||
|
read-only and tenant-safe.
|
||||||
|
parameters:
|
||||||
|
- in: query
|
||||||
|
name: tenant
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Optional tenant prefilter using the existing tenant id or external id
|
||||||
|
pattern already accepted by the workspace page.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Workspace page rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewWorkspacePageModel'
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, actors without entitled tenants, or explicit out-of-scope tenant targeting
|
||||||
|
|
||||||
|
/admin/t/{tenant}/reviews/{review}:
|
||||||
|
get:
|
||||||
|
summary: Open the mapped control explanation for a released review
|
||||||
|
description: |
|
||||||
|
Existing tenant-scoped released-review detail route reused as the secondary
|
||||||
|
context surface from the customer review workspace. The customer-workspace
|
||||||
|
flow uses the existing `customer_workspace=1` query flag to keep the detail
|
||||||
|
read-only and customer-safe.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tenant
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: path
|
||||||
|
name: review
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: customer_workspace
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: boolean
|
||||||
|
description: Existing query-context flag that suppresses operator lifecycle actions on the detail surface.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Released review detail rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewDetailModel'
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, tenant mismatches, or out-of-scope review targets
|
||||||
|
|
||||||
|
/admin/t/{tenant}/evidence/{evidenceSnapshot}:
|
||||||
|
get:
|
||||||
|
summary: Open supporting evidence from a mapped control explanation
|
||||||
|
description: |
|
||||||
|
Existing tenant-scoped evidence detail route reused only after explicit
|
||||||
|
drilldown from the released-review detail surface and existing capability
|
||||||
|
checks.
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: tenant
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: path
|
||||||
|
name: evidenceSnapshot
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
- in: query
|
||||||
|
name: source_surface
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
description: Existing source-surface metadata hook reused by the shared audit path.
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Evidence proof detail rendered
|
||||||
|
content:
|
||||||
|
text/html:
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
'403':
|
||||||
|
description: Forbidden for an in-scope actor missing the evidence capability
|
||||||
|
'404':
|
||||||
|
description: Not found for non-members, mismatched tenant scope, or unavailable evidence targets
|
||||||
|
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
ControlInterpretationVersion:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- version_key
|
||||||
|
- display_label
|
||||||
|
- non_certification_disclosure
|
||||||
|
properties:
|
||||||
|
version_key:
|
||||||
|
type: string
|
||||||
|
example: compliance_evidence_mapping.v1
|
||||||
|
display_label:
|
||||||
|
type: string
|
||||||
|
non_certification_disclosure:
|
||||||
|
type: string
|
||||||
|
|
||||||
|
AccessState:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- state
|
||||||
|
- message
|
||||||
|
properties:
|
||||||
|
state:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- available
|
||||||
|
- absent
|
||||||
|
- unavailable
|
||||||
|
- expired
|
||||||
|
- redacted
|
||||||
|
- partial
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
url:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
CustomerControlSummary:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- control_key
|
||||||
|
- control_name
|
||||||
|
- readiness_bucket
|
||||||
|
- limitation_flags
|
||||||
|
- customer_summary
|
||||||
|
- recommended_next_action
|
||||||
|
properties:
|
||||||
|
control_key:
|
||||||
|
type: string
|
||||||
|
control_name:
|
||||||
|
type: string
|
||||||
|
domain_key:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
readiness_bucket:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- follow_up_required
|
||||||
|
- review_recommended
|
||||||
|
- evidence_on_record
|
||||||
|
limitation_flags:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- accepted_risk_influenced
|
||||||
|
- partial_mapping
|
||||||
|
- stale_evidence
|
||||||
|
- supporting_evidence_unavailable
|
||||||
|
- unmapped
|
||||||
|
customer_summary:
|
||||||
|
type: string
|
||||||
|
evidence_basis_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
accepted_risk_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
recommended_next_action:
|
||||||
|
type: string
|
||||||
|
detail_anchor:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
CustomerControlExplanation:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/CustomerControlSummary'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
explanation_text:
|
||||||
|
type: string
|
||||||
|
evidence_basis_items:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
proof_access:
|
||||||
|
$ref: '#/components/schemas/AccessState'
|
||||||
|
|
||||||
|
CustomerReviewWorkspaceEntry:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- tenant_id
|
||||||
|
- tenant_name
|
||||||
|
- latest_published_review_id
|
||||||
|
- latest_review_published_at
|
||||||
|
- interpretation
|
||||||
|
- control_summaries
|
||||||
|
properties:
|
||||||
|
tenant_id:
|
||||||
|
type: integer
|
||||||
|
tenant_name:
|
||||||
|
type: string
|
||||||
|
latest_published_review_id:
|
||||||
|
type: integer
|
||||||
|
latest_review_published_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
interpretation:
|
||||||
|
$ref: '#/components/schemas/ControlInterpretationVersion'
|
||||||
|
control_summaries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomerControlSummary'
|
||||||
|
follow_up_summary:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
CustomerReviewWorkspacePageModel:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- workspace_id
|
||||||
|
- entries
|
||||||
|
properties:
|
||||||
|
workspace_id:
|
||||||
|
type: integer
|
||||||
|
tenant_filter_id:
|
||||||
|
type: integer
|
||||||
|
nullable: true
|
||||||
|
entries:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomerReviewWorkspaceEntry'
|
||||||
|
empty_state_message:
|
||||||
|
type: string
|
||||||
|
nullable: true
|
||||||
|
|
||||||
|
CustomerReviewDetailModel:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- review_id
|
||||||
|
- tenant_id
|
||||||
|
- customer_workspace_context
|
||||||
|
- interpretation
|
||||||
|
- controls
|
||||||
|
- operator_actions_hidden
|
||||||
|
properties:
|
||||||
|
review_id:
|
||||||
|
type: integer
|
||||||
|
tenant_id:
|
||||||
|
type: integer
|
||||||
|
customer_workspace_context:
|
||||||
|
type: boolean
|
||||||
|
interpretation:
|
||||||
|
$ref: '#/components/schemas/ControlInterpretationVersion'
|
||||||
|
controls:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/CustomerControlExplanation'
|
||||||
|
operator_actions_hidden:
|
||||||
|
type: boolean
|
||||||
|
supporting_evidence_collapsed_by_default:
|
||||||
|
type: boolean
|
||||||
|
raw_support_details_hidden_by_default:
|
||||||
|
type: boolean
|
||||||
341
specs/259-compliance-evidence-mapping/data-model.md
Normal file
341
specs/259-compliance-evidence-mapping/data-model.md
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
# Data Model — Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Spec**: [spec.md](spec.md)
|
||||||
|
|
||||||
|
No new persisted table, report artifact family, or projection store is required for this feature. The implementation reuses current review, section, evidence, finding, accepted-risk, membership, and audit truth, then embeds one bounded interpretation contract inside the existing review payloads.
|
||||||
|
|
||||||
|
## Source Truth Reused
|
||||||
|
|
||||||
|
### Workspace / Tenant Entitlement Context
|
||||||
|
|
||||||
|
**Purpose**: Establish the active workspace boundary and entitled tenant set before any workspace row, released-review detail, or supporting evidence route is resolved.
|
||||||
|
|
||||||
|
**Persisted carriers**:
|
||||||
|
- existing workspace membership rows
|
||||||
|
- existing tenant membership pivot rows and role assignments
|
||||||
|
- existing capability registry and role-capability map
|
||||||
|
|
||||||
|
**Relevant fields / contracts**:
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- workspace membership existence
|
||||||
|
- tenant membership role
|
||||||
|
- capability grants derived from [../../apps/platform/app/Support/Auth/Capabilities.php](../../apps/platform/app/Support/Auth/Capabilities.php)
|
||||||
|
- remembered tenant context from the existing workspace session model
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- the actor must be a member of the current workspace or the request resolves as not found
|
||||||
|
- workspace rows and explicit tenant filters may only resolve for entitled tenants in that workspace
|
||||||
|
- out-of-scope tenant targets remain `404` and must not leak review or evidence presence
|
||||||
|
|
||||||
|
### CanonicalControlDefinition
|
||||||
|
|
||||||
|
**Purpose**: Existing provider-neutral control identity used as the anchor for every mapped customer-safe control summary.
|
||||||
|
|
||||||
|
**Carrier**: existing [../../apps/platform/config/canonical_controls.php](../../apps/platform/config/canonical_controls.php) loaded through [../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php)
|
||||||
|
|
||||||
|
**Relevant fields**:
|
||||||
|
- `control_key`
|
||||||
|
- `name`
|
||||||
|
- `domain_key`
|
||||||
|
- `subdomain_key`
|
||||||
|
- `summary`
|
||||||
|
- `control_class`
|
||||||
|
- `evaluation_strategy`
|
||||||
|
- `evidence_archetypes`
|
||||||
|
- `historical_status`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- canonical controls remain provider-neutral and customer-facing labels must continue to use the catalog's neutral names
|
||||||
|
- provider-specific Microsoft bindings stay internal resolution inputs only
|
||||||
|
- the interpretation overlay may add customer-safe readiness meaning, but it must not create a second control taxonomy
|
||||||
|
|
||||||
|
### Findings Summary Evidence Input
|
||||||
|
|
||||||
|
**Purpose**: Existing per-finding evidence basis that already resolves canonical control references and accepted-risk governance state.
|
||||||
|
|
||||||
|
**Persisted carriers**:
|
||||||
|
- existing `evidence_snapshots`
|
||||||
|
- existing `evidence_snapshot_items`
|
||||||
|
- existing `findings`
|
||||||
|
|
||||||
|
**Primary producer**: [../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php](../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php)
|
||||||
|
|
||||||
|
**Relevant fields / payload**:
|
||||||
|
- `EvidenceSnapshot.id`
|
||||||
|
- `EvidenceSnapshot.generated_at`
|
||||||
|
- `EvidenceSnapshot.expires_at`
|
||||||
|
- `EvidenceSnapshotItem.dimension_key = findings_summary`
|
||||||
|
- `summary_payload.entries[].id`
|
||||||
|
- `summary_payload.entries[].status`
|
||||||
|
- `summary_payload.entries[].severity`
|
||||||
|
- `summary_payload.entries[].terminal_outcome`
|
||||||
|
- `summary_payload.entries[].canonical_control_resolution`
|
||||||
|
- `summary_payload.entries[].governance_state`
|
||||||
|
- `summary_payload.entries[].governance_warning`
|
||||||
|
- `summary_payload.canonical_controls[]`
|
||||||
|
- `summary_payload.risk_acceptance`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- canonical control identity continues to come from the current upstream resolution path only
|
||||||
|
- the interpretation overlay should consume current governance-state and terminal-outcome values rather than inventing a second status source
|
||||||
|
- if the current payload is sufficient, upstream evidence collection remains unchanged
|
||||||
|
|
||||||
|
### FindingException / Accepted Risk Decision
|
||||||
|
|
||||||
|
**Purpose**: Existing accepted-risk and accountability truth that can weaken or qualify a customer-safe control interpretation.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `finding_exceptions` rows via [../../apps/platform/app/Models/FindingException.php](../../apps/platform/app/Models/FindingException.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `status`
|
||||||
|
- `current_validity_state`
|
||||||
|
- `owner_user_id`
|
||||||
|
- `approved_by_user_id`
|
||||||
|
- `request_reason`
|
||||||
|
- `review_due_at`
|
||||||
|
- `effective_from`
|
||||||
|
- `expires_at`
|
||||||
|
- `owner`
|
||||||
|
- `approver`
|
||||||
|
- `currentDecision`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- accepted-risk truth may qualify a control interpretation but must not collapse into a fully positive readiness claim
|
||||||
|
- missing owner, approver, or review-due truth must surface as explicit partial disclosure rather than fabricated certainty
|
||||||
|
- this slice remains read-only and does not introduce approval, renewal, or revocation actions
|
||||||
|
|
||||||
|
### TenantReview
|
||||||
|
|
||||||
|
**Purpose**: Existing released review artifact that anchors the shared interpretation contract for both workspace and detail surfaces.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `tenant_reviews` rows via [../../apps/platform/app/Models/TenantReview.php](../../apps/platform/app/Models/TenantReview.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `status`
|
||||||
|
- `generated_at`
|
||||||
|
- `published_at`
|
||||||
|
- `summary`
|
||||||
|
- `evidence_snapshot_id`
|
||||||
|
- `current_export_review_pack_id`
|
||||||
|
- `operation_run_id`
|
||||||
|
- `tenant`
|
||||||
|
- `evidenceSnapshot`
|
||||||
|
- `sections`
|
||||||
|
|
||||||
|
**Planned embedded summary additions inside the existing `summary` JSON**:
|
||||||
|
- `control_interpretation.version`
|
||||||
|
- `control_interpretation.display_label`
|
||||||
|
- `control_interpretation.non_certification_disclosure`
|
||||||
|
- `control_interpretation.controls[]`
|
||||||
|
- `control_interpretation.limitations[]`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- only released reviews feed the default customer-safe workspace path
|
||||||
|
- the shared interpretation version must be carried at compose time so older released reviews preserve the meaning shown at publication time
|
||||||
|
- the workspace must read the embedded summary contract instead of recomputing the interpretation locally
|
||||||
|
|
||||||
|
### TenantReviewSection
|
||||||
|
|
||||||
|
**Purpose**: Existing detailed review section carrier used for the per-control explanation surface on the released review detail page.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `tenant_review_sections` rows via [../../apps/platform/app/Models/TenantReviewSection.php](../../apps/platform/app/Models/TenantReviewSection.php)
|
||||||
|
|
||||||
|
**Relevant fields**:
|
||||||
|
- `section_key`
|
||||||
|
- `title`
|
||||||
|
- `sort_order`
|
||||||
|
- `completeness_state`
|
||||||
|
- `summary_payload`
|
||||||
|
- `render_payload`
|
||||||
|
- `measured_at`
|
||||||
|
|
||||||
|
**Planned usage**:
|
||||||
|
- add one new v1 interpretation section inside the existing section family with the canonical `control_interpretation` section key
|
||||||
|
- keep the section near the top of the customer-workspace detail disclosure so mapped-control explanation precedes rawer supporting sections
|
||||||
|
|
||||||
|
**Planned embedded payload shape**:
|
||||||
|
- `summary_payload.version`
|
||||||
|
- `summary_payload.mapped_control_count`
|
||||||
|
- `summary_payload.follow_up_required_count`
|
||||||
|
- `summary_payload.limitation_counts`
|
||||||
|
- `render_payload.entries[]` containing detailed per-control explanations
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- exactly one interpretation section should exist per review for v1
|
||||||
|
- the detail surface should read this section instead of deriving a second explanation contract in-page
|
||||||
|
- this remains part of the existing review artifact and does not create a new section table or entity family
|
||||||
|
|
||||||
|
### EvidenceSnapshot
|
||||||
|
|
||||||
|
**Purpose**: Existing supporting proof artifact that informs evidence-basis summaries and explicit deeper drilldown.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `evidence_snapshots` rows via [../../apps/platform/app/Models/EvidenceSnapshot.php](../../apps/platform/app/Models/EvidenceSnapshot.php)
|
||||||
|
|
||||||
|
**Relevant fields / relationships**:
|
||||||
|
- `id`
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `status`
|
||||||
|
- `completeness_state`
|
||||||
|
- `generated_at`
|
||||||
|
- `expires_at`
|
||||||
|
- `summary`
|
||||||
|
- `items`
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- supporting proof remains optional, lower-priority, and capability-gated
|
||||||
|
- the overlay may summarize the evidence basis, but raw payloads and unrestricted diagnostics remain outside the default customer-safe path
|
||||||
|
- supporting evidence access should reuse the existing evidence route and audit action
|
||||||
|
|
||||||
|
### Audit Log Event Family
|
||||||
|
|
||||||
|
**Purpose**: Existing audit trail used to keep the displayed interpretation and access path traceable.
|
||||||
|
|
||||||
|
**Persisted carrier**: existing `audit_logs` rows via [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php)
|
||||||
|
|
||||||
|
**Relevant current action IDs**:
|
||||||
|
- `customer_review_workspace.opened`
|
||||||
|
- `tenant_review.opened`
|
||||||
|
- `evidence_snapshot.opened`
|
||||||
|
|
||||||
|
**Planned shared metadata additions**:
|
||||||
|
- `source_surface`
|
||||||
|
- `interpretation_version`
|
||||||
|
- `review_id` when a released review is opened from the workspace
|
||||||
|
- `tenant_filter_id` when the workspace is entered with a safe prefilter
|
||||||
|
|
||||||
|
**Validation / usage rules**:
|
||||||
|
- no new audit store or audit event family is justified
|
||||||
|
- interpretation-version traceability should be added to shared metadata before introducing any new event concept
|
||||||
|
|
||||||
|
## Embedded Interpretation Contracts
|
||||||
|
|
||||||
|
### ControlInterpretationOverlayVersion
|
||||||
|
|
||||||
|
**Purpose**: The explicit version label that states which customer-safe interpretation rules produced the displayed control/readiness summaries.
|
||||||
|
|
||||||
|
**Persistence**: embedded in existing `TenantReview.summary` and the existing interpretation section payload; echoed in shared audit metadata when relevant
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `version_key` (planned baseline: `compliance_evidence_mapping.v1`)
|
||||||
|
- `display_label`
|
||||||
|
- `non_certification_disclosure`
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- exactly one version key is carried for a given released review
|
||||||
|
- the version must be visible on workspace and detail surfaces whenever mapped control summaries are shown
|
||||||
|
|
||||||
|
### CustomerControlSummary
|
||||||
|
|
||||||
|
**Purpose**: Compact per-control summary reused by the workspace and as the top-level contract for the detail surface.
|
||||||
|
|
||||||
|
**Persistence**: embedded inside `TenantReview.summary['control_interpretation']['controls']`
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `control_key`
|
||||||
|
- `control_name`
|
||||||
|
- `domain_key`
|
||||||
|
- `readiness_bucket`
|
||||||
|
- `limitation_flags[]`
|
||||||
|
- `customer_summary`
|
||||||
|
- `evidence_basis_summary`
|
||||||
|
- `accepted_risk_summary` (nullable)
|
||||||
|
- `recommended_next_action`
|
||||||
|
- `detail_anchor` (nullable)
|
||||||
|
- `supporting_finding_ids[]`
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- one summary exists per mapped canonical control for the released review
|
||||||
|
- the summary must stay customer-safe and must not expose provider IDs, raw JSON, or support-only diagnostics
|
||||||
|
- the same summary meaning must appear on the workspace and detail surfaces for the same released review
|
||||||
|
|
||||||
|
### CustomerControlExplanation
|
||||||
|
|
||||||
|
**Purpose**: Detailed per-control explanation rendered on the released-review detail surface.
|
||||||
|
|
||||||
|
**Persistence**: embedded inside the interpretation `TenantReviewSection.render_payload.entries[]`
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `control_key`
|
||||||
|
- `control_name`
|
||||||
|
- `readiness_bucket`
|
||||||
|
- `limitation_flags[]`
|
||||||
|
- `explanation_text`
|
||||||
|
- `evidence_basis_items[]`
|
||||||
|
- `accepted_risk_context` (nullable)
|
||||||
|
- `recommended_next_action`
|
||||||
|
- `proof_access_state`
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- the detail explanation must be a denser expansion of the compact summary, not a second source of truth
|
||||||
|
- supporting evidence remains explicit drilldown, not default-visible raw detail
|
||||||
|
- the explanation must keep non-certification language visible and must not claim legal or framework attestation
|
||||||
|
|
||||||
|
### CustomerReviewWorkspaceEntry
|
||||||
|
|
||||||
|
**Purpose**: Derived row-level presentation contract for one entitled tenant with a released review on the workspace page.
|
||||||
|
|
||||||
|
**Persistence**: none; derived at request time from the latest released `TenantReview` and its embedded interpretation contract
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `workspace_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `tenant_name`
|
||||||
|
- `latest_published_review_id`
|
||||||
|
- `latest_review_published_at`
|
||||||
|
- `interpretation_version`
|
||||||
|
- `control_summaries[]`
|
||||||
|
- `follow_up_summary`
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- exactly one derived entry exists per entitled tenant with a released review visible in the current workspace scope
|
||||||
|
- when no entitled tenant has a released review, the workspace falls back to a page-level empty state rather than emitting partial row contracts without a review
|
||||||
|
- the one dominant row action remains opening the released review; supporting proof stays secondary and is not a peer primary action on the row
|
||||||
|
|
||||||
|
### CustomerReviewDetailContext
|
||||||
|
|
||||||
|
**Purpose**: Derived detail-page contract for the existing released-review surface when launched from the workspace.
|
||||||
|
|
||||||
|
**Persistence**: none; derived from the released `TenantReview`, its interpretation section, and the current `customer_workspace` query flag
|
||||||
|
|
||||||
|
**Fields**:
|
||||||
|
- `review_id`
|
||||||
|
- `tenant_id`
|
||||||
|
- `customer_workspace_context` (boolean)
|
||||||
|
- `interpretation_version`
|
||||||
|
- `controls[]`
|
||||||
|
- `non_certification_disclosure`
|
||||||
|
- `operator_actions_hidden` (boolean)
|
||||||
|
- `supporting_evidence_collapsed_by_default` (boolean)
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- the detail page must stay read-only in customer-workspace context
|
||||||
|
- the same interpretation version and control meaning shown on the workspace must be visible here
|
||||||
|
- supporting evidence should remain explicit in-body drilldown rather than a new dominant header action
|
||||||
|
|
||||||
|
## Derived Disclosure States
|
||||||
|
|
||||||
|
This feature introduces no new platform-wide lifecycle family. It does require one bounded overlay-local readiness contract and explicit limitation flags.
|
||||||
|
|
||||||
|
### Primary readiness buckets
|
||||||
|
|
||||||
|
- `follow_up_required`
|
||||||
|
- `review_recommended`
|
||||||
|
- `evidence_on_record`
|
||||||
|
|
||||||
|
### Limitation flags
|
||||||
|
|
||||||
|
- `accepted_risk_influenced`
|
||||||
|
- `partial_mapping`
|
||||||
|
- `stale_evidence`
|
||||||
|
- `supporting_evidence_unavailable`
|
||||||
|
- `unmapped`
|
||||||
|
|
||||||
|
**Validation rules**:
|
||||||
|
- limitation flags qualify the customer-safe interpretation and must never be silently collapsed into `evidence_on_record`
|
||||||
|
- accepted risk remains a qualifier, not a passing state
|
||||||
|
- these values stay embedded in the interpretation contract only and do not become a broader platform taxonomy or standalone persistence family
|
||||||
308
specs/259-compliance-evidence-mapping/plan.md
Normal file
308
specs/259-compliance-evidence-mapping/plan.md
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
# Implementation Plan: Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Branch**: `259-compliance-evidence-mapping` | **Date**: 2026-04-30 | **Spec**: [spec.md](spec.md)
|
||||||
|
**Input**: Feature specification from [spec.md](spec.md)
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Add one bounded, versioned interpretation overlay over the canonical control references already flowing through tenant review composition, then reuse that same overlay in the existing [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) page and the existing released-review detail flow anchored by [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php). The narrow implementation path is to derive the customer-safe control meaning once from existing review, finding, accepted-risk, and evidence truth, carry the versioned result inside the existing `TenantReview` summary and section payloads, and render those same payloads on both surfaces.
|
||||||
|
|
||||||
|
This remains an admin-plane Filament v5 surface running on Livewire v4. Provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php) with no change expected. No new panel, provider, OperationRun flow, destructive action, persistence table, report engine, portal, global-search scope, or asset strategy change is planned. Governance-as-a-Service Packaging and framework-specific overlays remain explicitly deferred.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4, Laravel 12
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing canonical-control, evidence, tenant-review, RBAC, localization, and audit seams
|
||||||
|
**Storage**: PostgreSQL via existing `tenant_reviews`, `tenant_review_sections`, `evidence_snapshots`, `evidence_snapshot_items`, `findings`, `finding_exceptions`, memberships, and `audit_logs`; no new persistence table planned
|
||||||
|
**Testing**: Pest v4 feature coverage plus one bounded browser smoke on the existing customer review workspace flow
|
||||||
|
**Validation Lanes**: confidence, browser
|
||||||
|
**Target Platform**: Laravel monolith in `apps/platform`, existing `/admin` workspace surface plus existing tenant-scoped `/admin/t/{tenant}` detail and proof reuse
|
||||||
|
**Project Type**: Web application (Laravel monolith with Filament pages and resources)
|
||||||
|
**Performance Goals**: derive the overlay from already-composed review/evidence truth, keep render-time queries tenant-safe and eager-loaded, avoid new Graph calls, and avoid new queue starts during page render
|
||||||
|
**Constraints**: one bounded versioned overlay only; no new table; no parallel report engine; no panel/provider change; no OperationRun UX; no destructive or authoring actions; no global-search expansion; no asset strategy change; no Governance-as-a-Service packaging or framework-specific overlay work
|
||||||
|
**Scale/Scope**: one shared overlay version, one existing workspace page, one existing released-review detail flow, existing evidence proof routes, existing DE/EN localization files, existing audit metadata, and focused expansion of the current review/evidence/browser tests
|
||||||
|
|
||||||
|
## Likely Affected Repo Surfaces
|
||||||
|
|
||||||
|
- [../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php) and [../../apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php) as the existing provider-neutral control identity, name, summary, and evidence metadata reused by the overlay.
|
||||||
|
- [../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php) and [../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php](../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php) as the only current control-resolution path feeding review evidence; this feature should not create a second resolver.
|
||||||
|
- [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php) to derive one customer-safe control interpretation section inside the existing review section family.
|
||||||
|
- [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php) to carry `interpretation_version` plus compact mapped-control summaries into the existing `TenantReview.summary` payload so workspace and detail surfaces stay aligned.
|
||||||
|
- [../../apps/platform/app/Models/TenantReview.php](../../apps/platform/app/Models/TenantReview.php) and [../../apps/platform/app/Models/TenantReviewSection.php](../../apps/platform/app/Models/TenantReviewSection.php) for narrow helper access to the stored overlay contract without adding a new persisted entity family.
|
||||||
|
- [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php) to keep workspace access on the current latest-published and tenant-entitlement seams.
|
||||||
|
- [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) and [../../apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php](../../apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php) for mapped-control summary rows, interpretation-version disclosure, safe tenant-prefilter reuse, and one dominant `Open released review` path.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/TenantReviewResource.php](../../apps/platform/app/Filament/Resources/TenantReviewResource.php) and [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) for the detailed per-control explanation path, customer-workspace read-only mode, and in-body supporting-evidence placement.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php) and existing evidence detail pages only as reused supporting-proof routes when the actor explicitly drills deeper and has the current capability.
|
||||||
|
- [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) and [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) for shared audit metadata tying workspace entry, review open, and evidence open events back to the interpretation version.
|
||||||
|
- [../../apps/platform/lang/en/localization.php](../../apps/platform/lang/en/localization.php) and [../../apps/platform/lang/de/localization.php](../../apps/platform/lang/de/localization.php) for localization-ready customer-safe wording and non-certification disclosure.
|
||||||
|
- [../../apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php), [../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php), [../../apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php](../../apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php), [../../apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php](../../apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php), and [../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php](../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php) for the bounded proof surface already in the repo.
|
||||||
|
|
||||||
|
## Shared Interpretation Path
|
||||||
|
|
||||||
|
- Keep canonical control resolution exactly where it already happens today: upstream in evidence collection via [../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php](../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php) and [../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php). The UI must not grow a second control-resolution path.
|
||||||
|
- Introduce one fixed v1 interpretation helper adjacent to the governance controls or tenant-review composition seams, not a generic registry or framework engine. Its only job is to turn existing canonical control references plus review, finding, accepted-risk, and evidence truth into one customer-safe control summary contract with a version label.
|
||||||
|
- Persist the shared result inside existing review truth so both surfaces read the same meaning: a compact control-summary block in `TenantReview.summary` for workspace consumption and one detailed control-explanation section in `TenantReviewSection` for released-review detail.
|
||||||
|
- Keep the overlay version explicit inside that shared payload and reuse it in audit metadata when the actor opens the workspace, opens a released review from that workspace, or drills into supporting evidence.
|
||||||
|
- Preserve one dominant inspect path. The workspace remains scan-first and opens the released review. Supporting evidence or export affordances must not compete with that path and should stay secondary, preferably inside the detail surface rather than as a second peer action on the list.
|
||||||
|
|
||||||
|
## UI / Filament & Livewire Fit
|
||||||
|
|
||||||
|
- Keep [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) as the primary decision surface. No new page, Resource, cluster, panel, portal, or provider is introduced.
|
||||||
|
- Keep the existing customer-workspace query-flag path on [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) as the secondary context surface. It should deepen the mapped control meaning without reopening operator lifecycle controls.
|
||||||
|
- Preserve Filament v5 plus Livewire v4 admin-plane patterns. Provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php) with no change expected.
|
||||||
|
- Preserve one dominant next action. On the workspace that remains `Open released review`. On the released-review detail surface, the mapped-control explanation is primary and supporting evidence stays in-body and capability-gated rather than becoming a new competing header action.
|
||||||
|
- Keep all touched resources non-search-expanding. `TenantReviewResource`, `ReviewPackResource`, and `EvidenceSnapshotResource` are already globally disabled, and this feature does not change that posture.
|
||||||
|
- No destructive actions are introduced. If existing non-customer detail actions are touched to preserve the customer-workspace mode, they remain outside this slice and continue to rely on `->action(...)`, `->requiresConfirmation()`, and existing authorization when used in their normal operator context.
|
||||||
|
|
||||||
|
## RBAC / Policy Fit
|
||||||
|
|
||||||
|
- Workspace membership remains the first isolation boundary through [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php) and current workspace context.
|
||||||
|
- Tenant entitlement remains capability-first and reuse-only. This slice does not add a new role family, raw capability strings, or customer-only policy branch.
|
||||||
|
- Non-members and out-of-scope tenant targets remain `404`. Inside an established scope, optional secondary evidence routes may still return explicit capability denial when the actor lacks the supporting evidence capability.
|
||||||
|
- The workspace continues to show only the latest published review per entitled tenant. Internal-only, draft, or ready-but-unreleased reviews remain outside the default customer-safe path.
|
||||||
|
- Supporting evidence drilldown stays on existing tenant-scoped evidence routes and current policy enforcement. The mapped control overlay does not widen discovery or expose new artifact classes.
|
||||||
|
|
||||||
|
## Audit / Logging Fit
|
||||||
|
|
||||||
|
- Reuse [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) and [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) as the only audit path.
|
||||||
|
- Existing `customer_review_workspace.opened`, `tenant_review.opened`, and `evidence_snapshot.opened` action IDs are sufficient for the current slice. The narrow follow-up, if needed, is metadata enrichment rather than a new event family.
|
||||||
|
- Planned shared audit metadata should include the source surface, active tenant prefilter when present, review identifier when relevant, and the interpretation version shown to the actor so FR-014 remains traceable without a new audit store.
|
||||||
|
- Passive render should stay quiet. Audit boundaries remain explicit workspace entry, explicit released-review open, and explicit evidence open.
|
||||||
|
|
||||||
|
## Data & Query Fit
|
||||||
|
|
||||||
|
- Keep canonical control references anchored to existing `findings_summary` evidence entries and [../../apps/platform/app/Models/TenantReview.php](../../apps/platform/app/Models/TenantReview.php)`::canonicalControlReferences()` rather than recomputing control identity in page code.
|
||||||
|
- Add the customer-safe interpretation as one bounded embedded contract inside existing review payloads. The likely shape is a compact `summary['control_interpretation']` block for workspace use plus one detailed `TenantReviewSection` entry for released-review detail.
|
||||||
|
- Carry the version identifier with the shared payload at compose time so older published reviews preserve the interpretation version that produced them. Avoid render-time reinterpretation drift between workspace and detail surfaces.
|
||||||
|
- Keep limitation states explicit but bounded. `unmapped`, `partial`, `stale`, `unavailable`, and `accepted-risk-influenced` should remain overlay-local derived semantics, not a new platform-wide lifecycle/state framework.
|
||||||
|
- Keep supporting proof anchored to existing `EvidenceSnapshot` truth and current evidence routes. The overlay should summarize the evidence basis, not create a second proof model.
|
||||||
|
- Avoid touching [../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php](../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php) unless implementation proves a missing normalized field blocks a single shared interpretation pass. The default plan is to consume its current canonical-control and governance-state output as-is.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: changed surfaces
|
||||||
|
- **Native vs custom classification summary**: native Filament
|
||||||
|
- **Shared-family relevance**: status messaging, review summaries, detail disclosure, evidence viewers, recommendation phrasing, and navigation entry points
|
||||||
|
- **State layers in scope**: page, detail, URL-query, table/session restore
|
||||||
|
- **Audience modes in scope**: customer/read-only, customer-admin, auditor-read-only, operator-MSP
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
|
||||||
|
- **Raw/support gating plan**: collapsed by default and capability-gated on reused evidence/detail routes only
|
||||||
|
- **One-primary-action / duplicate-truth control**: the workspace keeps one dominant `Open released review` path, the detail page stays explanation-first, and the mapped control meaning is authored once in the shared overlay contract rather than duplicated in page-local mappers
|
||||||
|
- **Handling modes by drift class or surface**: review-mandatory
|
||||||
|
- **Repository-signal treatment**: review-mandatory
|
||||||
|
- **Special surface test profiles**: standard-native-filament, shared-detail-family
|
||||||
|
- **Required tests or manual smoke**: functional-core, state-contract, bounded-browser-smoke
|
||||||
|
- **Exception path and spread control**: none planned; any proposal for a page-local mapper, new portal surface, or framework-specific overlay becomes exception-required drift
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes
|
||||||
|
- **Systems touched**: `CanonicalControlCatalog`, `CanonicalControlResolver`, `FindingsSummarySource`, `TenantReviewSectionFactory`, `TenantReviewComposer`, `CustomerReviewWorkspace`, `TenantReviewResource`, `ViewTenantReview`, `EvidenceSnapshotResource`, `WorkspaceAuditLogger`, `AuditActionId`, and review localization copy
|
||||||
|
- **Shared abstractions reused**: canonical control definitions and resolver output, existing review summary and section payloads, existing workspace and tenant-scoped routes, current capability checks, and the shared audit logger
|
||||||
|
- **New abstraction introduced? why?**: one bounded v1 interpretation helper is expected because current repo seams identify canonical controls but do not yet produce a customer-safe, versioned control/readiness contract reusable across surfaces
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: existing seams already provide the released review truth, control references, accepted-risk signals, evidence basis, and UI surfaces; what is missing is one shared interpretation contract, not a new persistence or routing foundation
|
||||||
|
- **Bounded deviation / spread control**: keep the interpretation helper fixed to one overlay version and current review surfaces; packaging and framework-specific overlays are explicitly deferred to later specs
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no
|
||||||
|
- **Central contract reused**: `N/A`
|
||||||
|
- **Delegated UX behaviors**: `N/A`
|
||||||
|
- **Surface-owned behavior kept local**: read-only workspace, detail, and evidence rendering only
|
||||||
|
- **Queued DB-notification policy**: `N/A`
|
||||||
|
- **Terminal notification path**: `N/A`
|
||||||
|
- **Exception path**: none
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Provider-owned seams**: Microsoft-specific subject binding and signal resolution inside [../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php) and [../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php](../../apps/platform/app/Services/Evidence/Sources/FindingsSummarySource.php)
|
||||||
|
- **Platform-core seams**: interpretation version, customer-safe control readiness wording, evidence-basis phrasing, accepted-risk influence wording, recommendation phrasing, and audit metadata tying the shown interpretation back to the released review
|
||||||
|
- **Neutral platform terms / contracts preserved**: `canonical control`, `control readiness`, `evidence basis`, `accepted risk`, `recommendation`, and `interpretation version`
|
||||||
|
- **Retained provider-specific semantics and why**: provider-specific Microsoft bindings remain internal input truth only because they already exist upstream in canonical control resolution; they do not become customer-facing labels
|
||||||
|
- **Bounded extraction or follow-up path**: `document-in-feature` for the bounded v1 overlay; `follow-up-spec` for Governance-as-a-Service packaging or framework-specific overlays if later required
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||||
|
|
||||||
|
- Inventory-first / snapshot truth: PASS. The slice derives from existing evidence snapshots and released review truth only.
|
||||||
|
- Read/write separation: PASS. No new mutation, publication, regeneration, remediation, or destructive action is introduced.
|
||||||
|
- Graph contract path: PASS. No new Graph work, provider calls, or contract registry changes are part of this preparation slice.
|
||||||
|
- Deterministic capabilities: PASS. Existing capability registries and role maps remain authoritative.
|
||||||
|
- Workspace and tenant isolation: PASS. Workspace membership and tenant entitlement remain `404` boundaries.
|
||||||
|
- RBAC-UX plane separation: PASS. All changes remain in the existing `/admin` plane and current tenant-scoped detail/proof routes.
|
||||||
|
- Destructive confirmation standard: PASS by non-use. No destructive action is planned for the customer-safe path.
|
||||||
|
- Global search safety: PASS. `TenantReviewResource`, `ReviewPackResource`, and `EvidenceSnapshotResource` are already globally disabled, and no new searchable surface is introduced.
|
||||||
|
- OperationRun / Ops-UX: PASS by non-use. The feature starts no runs and changes no run lifecycle UX.
|
||||||
|
- Data minimization: PASS. Raw payloads, provider IDs, and support-only diagnostics remain hidden by default and only appear through existing gated drilldown.
|
||||||
|
- Test governance (TEST-GOV-001): PASS. Planned proof stays inside focused feature files plus one bounded browser smoke.
|
||||||
|
- Proportionality / no premature abstraction: PASS with constraint. One bounded v1 interpretation helper is justified because the spec requires a shared versioned meaning layer; a generic registry or multi-framework engine is explicitly out of bounds.
|
||||||
|
- Persisted truth (PERSIST-001): PASS. No new table or artifact family is planned; the versioned overlay is carried inside existing `TenantReview` and `TenantReviewSection` payloads only.
|
||||||
|
- Behavioral state (STATE-001): PASS. Readiness buckets and limitation markers stay overlay-local and serve customer-safe decision support; they are not elevated into a cross-domain lifecycle framework.
|
||||||
|
- UI semantics / shared pattern first / Filament-native UI: PASS. Existing Filament pages and review/evidence seams remain the default path, and the shared interpretation contract prevents page-local drift.
|
||||||
|
- Provider boundary (PROV-001): PASS. Provider-specific semantics stay inside existing resolution inputs and do not leak into customer-facing platform-core vocabulary.
|
||||||
|
- Filament / Laravel planning contract: PASS. Filament v5 stays on Livewire v4, provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no new panel/provider work is planned, no global-search expansion is planned, and asset handling remains unchanged.
|
||||||
|
- Explicit deferrals: PASS. Governance-as-a-Service Packaging and framework-specific overlays remain out of scope for this version.
|
||||||
|
|
||||||
|
**Gate evaluation**: PASS.
|
||||||
|
|
||||||
|
- The narrow path is defensible if the implementation derives the interpretation once during review composition and keeps both UI surfaces on that shared payload.
|
||||||
|
- The plan fails the gate if it drifts into page-local mappers, a new packaging/reporting engine, or a multi-framework taxonomy.
|
||||||
|
|
||||||
|
**Post-design re-check**: PASS once [research.md](research.md), [data-model.md](data-model.md), [quickstart.md](quickstart.md), and [contracts/compliance-evidence-mapping.openapi.yaml](contracts/compliance-evidence-mapping.openapi.yaml) are present and the agent-context update step is executed.
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: Feature for review composition, workspace summary, detail explanation, authorization, navigation context, and evidence drilldown; Browser for one bounded workspace-to-detail smoke
|
||||||
|
- **Affected validation lanes**: confidence, browser
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: the existing feature families already cover review composition, customer review workspace behavior, and detail-surface contracts; one existing browser smoke is enough to catch rendered disclosure regressions without creating a new browser family
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: moderate but contained; reuse existing tenant review, evidence snapshot, finding, finding-exception, workspace membership, and readonly-actor fixtures
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no; new helper use should stay explicit and local to the tenant-review and review-workspace family
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none beyond the already-existing single browser smoke
|
||||||
|
- **Surface-class relief / special coverage rule**: standard-native-filament relief on the workspace page, shared-detail-family coverage on released-review explanation and evidence drilldown
|
||||||
|
- **Closing validation and reviewer handoff**: rerun the focused commands above, verify the same interpretation version and control meaning appear on both surfaces, verify `404` isolation for out-of-scope tenant targets, verify capability-gated evidence drilldown, and verify non-certification wording stays visible
|
||||||
|
- **Budget / baseline / trend follow-up**: none expected beyond small feature-local additions in existing suites
|
||||||
|
- **Review-stop questions**: lane fit, hidden fixture growth, browser sprawl, page-local mapper drift, and framework-scope creep
|
||||||
|
- **Escalation path**: `document-in-feature` for contained metadata or helper-shape notes; `reject-or-split` for any drift into packaging, framework-specific overlays, or a second interpretation path
|
||||||
|
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||||
|
- **Why no dedicated follow-up spec is needed**: this package is already the bounded follow-up spec for the customer review productization lane; broader packaging and framework overlay work are explicitly deferred
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/259-compliance-evidence-mapping/
|
||||||
|
├── spec.md
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── compliance-evidence-mapping.openapi.yaml
|
||||||
|
└── tasks.md # Created later by /speckit.tasks, not by this plan step
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ ├── Pages/Reviews/
|
||||||
|
│ │ │ └── CustomerReviewWorkspace.php
|
||||||
|
│ │ └── Resources/
|
||||||
|
│ │ ├── TenantReviewResource.php
|
||||||
|
│ │ ├── TenantReviewResource/Pages/ViewTenantReview.php
|
||||||
|
│ │ └── EvidenceSnapshotResource.php
|
||||||
|
│ ├── Models/
|
||||||
|
│ │ ├── TenantReview.php
|
||||||
|
│ │ ├── TenantReviewSection.php
|
||||||
|
│ │ ├── EvidenceSnapshot.php
|
||||||
|
│ │ ├── EvidenceSnapshotItem.php
|
||||||
|
│ │ └── FindingException.php
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── Audit/WorkspaceAuditLogger.php
|
||||||
|
│ │ ├── Evidence/Sources/FindingsSummarySource.php
|
||||||
|
│ │ └── TenantReviews/
|
||||||
|
│ │ ├── TenantReviewComposer.php
|
||||||
|
│ │ ├── TenantReviewRegisterService.php
|
||||||
|
│ │ └── TenantReviewSectionFactory.php
|
||||||
|
│ ├── Support/
|
||||||
|
│ │ ├── Audit/AuditActionId.php
|
||||||
|
│ │ └── Governance/Controls/
|
||||||
|
│ │ ├── CanonicalControlCatalog.php
|
||||||
|
│ │ ├── CanonicalControlDefinition.php
|
||||||
|
│ │ └── CanonicalControlResolver.php
|
||||||
|
├── config/canonical_controls.php
|
||||||
|
├── lang/
|
||||||
|
│ ├── de/localization.php
|
||||||
|
│ └── en/localization.php
|
||||||
|
├── bootstrap/providers.php
|
||||||
|
└── tests/
|
||||||
|
├── Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php
|
||||||
|
├── Feature/Reviews/
|
||||||
|
└── Feature/TenantReview/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Laravel monolith. The implementation stays inside the existing `apps/platform` governance, review, evidence, localization, and audit seams, with no new panel/provider location and no new persistence layer.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| One bounded v1 interpretation helper and embedded payload shape | The spec requires one shared, versioned control/readiness meaning reused across workspace and detail surfaces | Page-local copy or two separate mappers would drift across surfaces and would not keep older released reviews traceable to one interpretation version |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: customer reviewers still have to translate canonical control references, findings, accepted-risk details, and evidence snapshots into a control/readiness story by hand.
|
||||||
|
- **Existing structure is insufficient because**: current catalog and resolver identify controls, but they do not yet produce one customer-safe, versioned interpretation contract reusable across workspace and detail surfaces.
|
||||||
|
- **Narrowest correct implementation**: introduce one fixed v1 interpretation helper, persist the derived result inside existing `TenantReview` summary and section payloads, and render that same payload on the existing workspace and released-review detail surfaces.
|
||||||
|
- **Ownership cost created**: maintain one overlay version label, one bounded readiness vocabulary, one detailed control explanation section, localized customer-safe copy, and focused review/evidence tests.
|
||||||
|
- **Alternative intentionally rejected**: page-local mappers, a multi-framework overlay engine, Governance-as-a-Service packaging, and a new reporting or portal surface were rejected because they import broad permanent complexity before one shared interpretation path is proven.
|
||||||
|
- **Release truth**: current-release truth. This is the next bounded follow-up after customer review productization, not speculative future infrastructure.
|
||||||
|
|
||||||
|
## Phase 0 — Research (output: research.md)
|
||||||
|
|
||||||
|
Research resolves the remaining implementation-shaping decisions:
|
||||||
|
|
||||||
|
- keep the existing customer review workspace and released-review detail routes as the only primary and secondary surfaces
|
||||||
|
- derive the interpretation once during tenant review composition and reuse it through existing review payloads instead of page-local mappers
|
||||||
|
- reuse canonical control catalog metadata and current findings-summary resolution rather than adding a second control taxonomy
|
||||||
|
- carry the version label through existing review and audit truth so older released reviews remain traceable
|
||||||
|
- keep supporting evidence on existing evidence routes and current capability checks
|
||||||
|
- keep Governance-as-a-Service packaging and framework-specific overlays explicitly deferred
|
||||||
|
- keep validation inside the current review/evidence feature families plus the single existing browser smoke
|
||||||
|
|
||||||
|
**Output**: [research.md](research.md)
|
||||||
|
|
||||||
|
## Phase 1 — Design (outputs: data-model.md, contracts/, quickstart.md)
|
||||||
|
|
||||||
|
Design artifacts capture the narrow interpretation shape:
|
||||||
|
|
||||||
|
- no new table, cache, or report artifact family; the overlay lives inside existing `TenantReview` and `TenantReviewSection` payloads only
|
||||||
|
- one compact workspace contract plus one detailed released-review explanation contract document how the same mapped control meaning is reused across surfaces
|
||||||
|
- one conceptual OpenAPI file documents the existing workspace, released-review detail, and supporting evidence routes that consume the shared overlay
|
||||||
|
- quickstart records the intended implementation order, validation commands, Filament v5 plus Livewire v4 posture, unchanged provider-registration location, and explicit deferrals
|
||||||
|
- `.specify/scripts/bash/update-agent-context.sh copilot` must run after the design artifacts are generated, even if it results in no technology changes
|
||||||
|
|
||||||
|
**Artifacts**:
|
||||||
|
|
||||||
|
- [data-model.md](data-model.md)
|
||||||
|
- [contracts/compliance-evidence-mapping.openapi.yaml](contracts/compliance-evidence-mapping.openapi.yaml)
|
||||||
|
- [quickstart.md](quickstart.md)
|
||||||
|
- Required agent-context update executed during planning on 2026-04-30 via `.specify/scripts/bash/update-agent-context.sh copilot`, refreshing `.github/agents/copilot-instructions.md` as the repo workflow expects.
|
||||||
|
|
||||||
|
## Phase 2 — Planning (for tasks.md)
|
||||||
|
|
||||||
|
Dependency-ordered outline for the later `tasks.md` step:
|
||||||
|
|
||||||
|
1. Define one fixed v1 interpretation helper near the canonical control or tenant-review composition seams, using existing control references, finding outcomes, accepted-risk truth, and evidence basis only.
|
||||||
|
2. Extend `TenantReviewSectionFactory` and `TenantReviewComposer` to persist `interpretation_version`, compact control summaries, and detailed per-control explanations into existing review payloads and sections.
|
||||||
|
3. Update `CustomerReviewWorkspace` and its Blade intro to read the shared overlay contract, show interpretation-version and non-certification disclosure, preserve tenant-safe prefilter behavior, and keep `Open released review` as the one dominant action.
|
||||||
|
4. Update the released-review detail surface to explain the same mapped control meaning, keep customer-workspace mode read-only, and move supporting evidence access into explicit in-body drilldown rather than competing primary actions.
|
||||||
|
5. Reuse existing evidence routes and audit events, enriching shared metadata with interpretation-version context where needed.
|
||||||
|
6. Expand the focused review, workspace, detail, evidence, and smoke tests without introducing a new browser family or a second interpretation path.
|
||||||
|
|
||||||
|
## Planning Guardrail Notes
|
||||||
|
|
||||||
|
- Planning guardrail result: PASS. Filament remains v5 on Livewire v4, provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no new panel/provider is expected, no global-search expansion is planned, no destructive action is introduced, and no new asset bundle is planned.
|
||||||
|
- Shared-path result: the plan keeps one interpretation contract reused across composition, workspace, and detail surfaces rather than duplicating logic in local mappers.
|
||||||
|
- Explicit deferral result: Governance-as-a-Service Packaging and framework-specific overlays remain follow-up specs and are not folded into this package.
|
||||||
|
- Preparation workflow result: the required agent-context refresh step has already been completed for this package and does not need a follow-on prep task.
|
||||||
|
|
||||||
|
## Implementation Close-Out
|
||||||
|
|
||||||
|
- Guardrail / Smoke Coverage result: PASS on 2026-04-30. The implementation stayed on Filament v5 plus Livewire v4, did not add a panel/provider/path, did not add assets, did not add persistence, did not add OperationRun behavior, and did not add destructive actions.
|
||||||
|
- Shared-interpretation-path outcome: PASS. `ComplianceEvidenceMappingV1` derives one stored `control_interpretation` contract during review composition; the workspace, released-review detail, and supporting evidence links read that stored payload instead of remapping controls locally.
|
||||||
|
- Audit-metadata reuse outcome: PASS. Existing `customer_review_workspace.opened`, `tenant_review.opened`, `evidence_snapshot.opened`, and review-pack download audit paths carry `source_surface`, `review_id` where applicable, `tenant_filter_id`, and `interpretation_version`; no new audit event family was required.
|
||||||
|
- Global-search safety outcome: PASS. `TenantReviewResource`, `ReviewPackResource`, and `EvidenceSnapshotResource` remain globally disabled; no searchable resource was added.
|
||||||
|
- List-surface review outcome: PASS. The customer review workspace shows entitled tenants with released reviews only, keeps no-released-review as a page-level empty state, shows visible version and non-certification disclosure, and keeps `Open released review` as the only row action.
|
||||||
|
- Document-in-feature decision: T023 closed as metadata reuse at existing call sites. `WorkspaceAuditLogger` and `AuditActionId` did not need code changes because the existing actions already cover the required audit moments.
|
||||||
|
- Follow-up-spec decisions: none required for this v1 implementation. Governance-as-a-Service packaging and framework-specific overlays remain explicitly deferred from this spec.
|
||||||
57
specs/259-compliance-evidence-mapping/quickstart.md
Normal file
57
specs/259-compliance-evidence-mapping/quickstart.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Quickstart — Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
- Docker is running and the Sail stack for `apps/platform` is available.
|
||||||
|
- The feature stays inside the existing Laravel monolith and existing admin plane.
|
||||||
|
- Filament remains v5 on Livewire v4.
|
||||||
|
- Panel providers remain registered through [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php); no provider or panel change is part of this work.
|
||||||
|
- No new persistence table, no new OperationRun flow, no new portal shell, no new report engine, no global-search expansion, and no asset strategy change are in scope.
|
||||||
|
- Governance-as-a-Service Packaging and framework-specific overlays remain deferred.
|
||||||
|
|
||||||
|
## Intended Implementation Order
|
||||||
|
|
||||||
|
1. Review the current canonical-control, findings-summary, tenant-review composition, workspace, detail, evidence, and feature-test seams so the change stays on one shared path.
|
||||||
|
2. Add one fixed v1 interpretation helper near the canonical control or tenant-review composition seams. Keep it single-purpose and versioned instead of building a generic overlay registry.
|
||||||
|
3. Extend [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php) and [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php) to embed `interpretation_version`, compact customer control summaries, and one detailed control-explanation section into the existing review payloads.
|
||||||
|
4. Add narrow access helpers on [../../apps/platform/app/Models/TenantReview.php](../../apps/platform/app/Models/TenantReview.php) if needed so both surfaces can read the same embedded contract without re-deriving it.
|
||||||
|
5. Update [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) and its Blade intro to render interpretation-version disclosure, customer-safe control summaries, explicit limitation states, and one dominant `Open released review` action.
|
||||||
|
6. Update [../../apps/platform/app/Filament/Resources/TenantReviewResource.php](../../apps/platform/app/Filament/Resources/TenantReviewResource.php) and [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) so the released-review detail explains the same mapped control meaning, stays read-only in customer-workspace mode, and keeps supporting evidence as capability-gated in-body drilldown.
|
||||||
|
7. Reuse existing evidence routes and shared audit events, enriching metadata with interpretation-version context where needed instead of inventing a new audit concept.
|
||||||
|
8. Update existing DE/EN localization keys for customer-safe wording and explicit non-certification disclosure.
|
||||||
|
9. Expand only the existing review, workspace, detail, evidence, and smoke tests.
|
||||||
|
10. Run the targeted tests and Pint after implementation.
|
||||||
|
|
||||||
|
## Targeted Validation Commands (after implementation)
|
||||||
|
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
|
||||||
|
## Planned Smoke Checklist (after implementation)
|
||||||
|
|
||||||
|
1. Sign in to `/admin` as a readonly-capable actor with workspace scope and open `/admin/reviews/workspace`.
|
||||||
|
2. Confirm only entitled tenants appear and that the default-visible path uses released reviews only.
|
||||||
|
3. Confirm the workspace shows the interpretation version, non-certification disclosure, control summaries, limitation states, and one dominant `Open released review` path.
|
||||||
|
4. Open a released review and confirm the same interpretation version and mapped control meaning appear on the detail surface.
|
||||||
|
5. Confirm raw payloads, provider IDs, fingerprints, and support-only diagnostics remain hidden by default in customer-workspace mode.
|
||||||
|
6. Drill into supporting evidence and confirm the route is capability-gated, tenant-safe, and still tied back to the customer-review flow.
|
||||||
|
7. Attempt an explicit out-of-scope tenant target and confirm the response remains not found without leaking tenant or review presence.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Implementation close-out on 2026-04-30: the package is implemented in the existing review, evidence, audit, localization, and test seams without adding new persistence, assets, providers, panels, OperationRun behavior, or destructive actions.
|
||||||
|
- Filament remains v5 on Livewire v4.
|
||||||
|
- Provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php) with no change expected.
|
||||||
|
- [../../apps/platform/app/Filament/Resources/TenantReviewResource.php](../../apps/platform/app/Filament/Resources/TenantReviewResource.php), [../../apps/platform/app/Filament/Resources/ReviewPackResource.php](../../apps/platform/app/Filament/Resources/ReviewPackResource.php), and [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php) remain globally disabled; this slice does not change their search posture.
|
||||||
|
- No destructive, authoring, publishing, generation, or remediation action belongs on the customer-safe mapped-control path.
|
||||||
|
- No new Filament assets are expected. If future implementation unexpectedly registers assets, deployment still requires `cd apps/platform && php artisan filament:assets`, but this package does not plan such a change.
|
||||||
|
- Governance-as-a-Service Packaging and framework-specific overlays stay outside this spec and should not be folded into implementation tasks for v1.
|
||||||
|
|
||||||
|
## Implementation Validation Results
|
||||||
|
|
||||||
|
- Focused review/evidence/browser regression: `./vendor/bin/sail artisan test --compact tests/Unit/TenantReview/TenantReviewComposerTest.php tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewAuditLogTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` passed with 51 tests and 323 assertions.
|
||||||
|
- Adjacent contract regression: `./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/Evidence/EvidenceSnapshotCanonicalControlReferenceTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackWidgetTest.php` passed with 43 tests and 225 assertions.
|
||||||
|
- Formatting: `./vendor/bin/sail bin pint --dirty --format agent` passed.
|
||||||
|
- Browser smoke path: tenant review detail → `Open customer workspace` → released-review workspace row → `Open latest review` → customer-workspace review detail, with no browser console or JavaScript errors.
|
||||||
153
specs/259-compliance-evidence-mapping/research.md
Normal file
153
specs/259-compliance-evidence-mapping/research.md
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
# Research — Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Date**: 2026-04-30
|
||||||
|
**Spec**: [spec.md](spec.md)
|
||||||
|
|
||||||
|
This document resolves the planning decisions for the smallest safe versioned interpretation overlay over the repo's existing canonical control references and released review truth.
|
||||||
|
|
||||||
|
## Decision 1 — Keep the existing customer review workspace and released-review detail flow as the only primary and secondary surfaces
|
||||||
|
|
||||||
|
**Decision**: Reuse [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) as the primary decision surface and [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) under the existing `customer_workspace` query flag as the only secondary context surface.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The repo already has the admin-plane route, tenant-safe prefilter behavior, customer-workspace query context, and bounded feature and browser tests for these surfaces.
|
||||||
|
- The missing piece is shared interpretation, not new routing, a new panel, or a portal shell.
|
||||||
|
- Reusing the existing surfaces keeps the feature aligned with the authoritative spec and avoids a second customer-review UX vocabulary.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a dedicated compliance portal or a second customer-only page.
|
||||||
|
- Rejected: duplicates the current review-consumption path and widens scope into shell-level IA.
|
||||||
|
- Push all explanation into the workspace only.
|
||||||
|
- Rejected: would leave the first drilldown without the detailed control explanation the spec requires.
|
||||||
|
|
||||||
|
## Decision 2 — Derive the interpretation once during tenant review composition and reuse it through existing review payloads
|
||||||
|
|
||||||
|
**Decision**: Materialize the mapped control/readiness contract during tenant review composition by extending [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php) and [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php), then reuse that stored result in both workspace and detail surfaces.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The spec requires the same mapped control meaning and interpretation version to remain consistent between workspace and released-review detail.
|
||||||
|
- Carrying the result inside existing `TenantReview` and `TenantReviewSection` payloads preserves traceability for older released reviews without a new persistence table.
|
||||||
|
- A single compose-time pass is the narrowest way to prevent page-level drift.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Compute the interpretation separately in each page.
|
||||||
|
- Rejected: two local mappers would drift and would not preserve older review history against the version used at release time.
|
||||||
|
- Add a new projection table or report artifact for the overlay.
|
||||||
|
- Rejected: violates the no-new-persistence constraint.
|
||||||
|
|
||||||
|
## Decision 3 — Keep canonical control identity on the existing catalog and resolver seams
|
||||||
|
|
||||||
|
**Decision**: Reuse [../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php), [../../apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php), and [../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php](../../apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php) as the only control identity and resolution path.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The catalog already exposes provider-neutral control keys, names, summaries, and domain metadata suitable for customer-safe labels.
|
||||||
|
- The resolver already maps Microsoft-owned evidence signals onto canonical controls upstream in evidence collection.
|
||||||
|
- The feature needs an interpretation overlay over canonical controls, not a second control taxonomy or framework registry.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a second customer-facing control catalog.
|
||||||
|
- Rejected: duplicates platform-core control truth and increases drift risk.
|
||||||
|
- Move provider-specific signal mapping into the UI.
|
||||||
|
- Rejected: deepens provider coupling in the wrong layer and violates the one-shared-path requirement.
|
||||||
|
|
||||||
|
## Decision 4 — Carry one explicit version label inside existing review truth and audit metadata
|
||||||
|
|
||||||
|
**Decision**: Use one explicit overlay version key, carried inside existing review payloads and surfaced anywhere the mapped control/readiness view appears. The planning baseline is a single v1 key such as `compliance_evidence_mapping.v1`.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- FR-003 and FR-014 require the interpretation version to be visible and traceable.
|
||||||
|
- Embedding the version into current review payloads preserves what an older released review meant at the time it was composed.
|
||||||
|
- The existing audit pipeline can carry the same version context without a new audit store.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Recompute the current version at render time only.
|
||||||
|
- Rejected: older released reviews could silently change meaning after later rule updates.
|
||||||
|
- Add a dedicated database column or separate version-history model.
|
||||||
|
- Rejected: broader persistence change than the current release truth needs.
|
||||||
|
|
||||||
|
## Decision 5 — Use a bounded readiness contract with explicit limitation flags instead of pass/fail or certification language
|
||||||
|
|
||||||
|
**Decision**: Keep the customer-safe interpretation contract bounded to a small readiness vocabulary plus explicit limitation flags. The planned primary readiness buckets are:
|
||||||
|
|
||||||
|
- `follow_up_required`
|
||||||
|
- `review_recommended`
|
||||||
|
- `evidence_on_record`
|
||||||
|
|
||||||
|
The planned limitation flags are:
|
||||||
|
|
||||||
|
- `accepted_risk_influenced`
|
||||||
|
- `partial_mapping`
|
||||||
|
- `stale_evidence`
|
||||||
|
- `supporting_evidence_unavailable`
|
||||||
|
- `unmapped`
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The feature must help a customer decide what needs follow-up without implying formal certification or legal compliance.
|
||||||
|
- A small readiness vocabulary with explicit limitation modifiers is easier to localize, easier to test, and less likely to overstate the evidence basis than a large scoring system.
|
||||||
|
- Keeping accepted risk as a modifier preserves the distinction between a governed exception and a fully positive readiness claim.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Use compliant / non-compliant / certified language.
|
||||||
|
- Rejected: overstates what the product can legitimately claim.
|
||||||
|
- Add a larger scorecard or framework-specific status ladder.
|
||||||
|
- Rejected: imports framework-engine complexity that the spec explicitly defers.
|
||||||
|
|
||||||
|
## Decision 6 — Keep supporting proof on the existing evidence routes and current capability checks
|
||||||
|
|
||||||
|
**Decision**: Reuse [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php) and existing evidence detail pages as the only supporting-proof routes for this slice.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- Existing evidence resources already enforce tenant scope, capability checks, and audit semantics.
|
||||||
|
- The spec calls for deeper proof only after explicit drilldown, not a new proof viewer or raw payload surface.
|
||||||
|
- Keeping proof access secondary preserves one dominant workspace action and avoids export/package redesign.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a new customer-only proof viewer.
|
||||||
|
- Rejected: duplicates evidence routing and widens scope.
|
||||||
|
- Put raw proof details directly on the workspace row.
|
||||||
|
- Rejected: violates the decision-first disclosure hierarchy.
|
||||||
|
|
||||||
|
## Decision 7 — Reuse existing audit action IDs and enrich metadata before creating new audit concepts
|
||||||
|
|
||||||
|
**Decision**: Keep the audit path on [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) and [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php), reusing `customer_review_workspace.opened`, `tenant_review.opened`, and `evidence_snapshot.opened` with interpretation-version metadata where needed.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The required access moments already exist as auditable actions in the repo.
|
||||||
|
- The remaining requirement is traceability of the interpretation version and source surface, which fits shared metadata better than a new event family.
|
||||||
|
- Reusing the current audit path preserves consistent redaction and workspace or tenant context handling.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Create a new compliance-evidence audit stream.
|
||||||
|
- Rejected: unnecessary persistence and ownership cost for a read-only slice.
|
||||||
|
- Leave version context unaudited.
|
||||||
|
- Rejected: weakens FR-014 traceability.
|
||||||
|
|
||||||
|
## Decision 8 — Keep validation inside the current review and evidence feature families plus the single existing browser smoke
|
||||||
|
|
||||||
|
**Decision**: Expand the existing review and evidence feature tests and keep [../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php](../../apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php) as the only browser proof.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The repo already has narrow feature files for canonical control references, workspace behavior, detail explanation, authorization, navigation context, and evidence routes.
|
||||||
|
- Those files are the cheapest honest proof for isolation, consistent wording, and cross-surface reuse.
|
||||||
|
- One bounded smoke is enough to catch rendered disclosure regressions without creating a new heavy test family.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Add a broader browser suite for every control state and evidence path.
|
||||||
|
- Rejected: too expensive for the bounded value of this slice.
|
||||||
|
- Rely only on unit tests around a new helper.
|
||||||
|
- Rejected: feature behavior and tenant-safe disclosure still need surface-level proof.
|
||||||
|
|
||||||
|
## Decision 9 — Keep Governance-as-a-Service Packaging and framework-specific overlays explicitly deferred
|
||||||
|
|
||||||
|
**Decision**: Treat this package as the foundational shared interpretation contract only. Governance-as-a-Service Packaging and framework-specific overlays remain follow-up work after the v1 overlay is stable.
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
- The spec explicitly defers both areas.
|
||||||
|
- Packaging work should consume a proven shared interpretation contract rather than shaping it prematurely.
|
||||||
|
- Framework-specific overlays would force broader taxonomy and copy decisions before one customer-safe path is validated.
|
||||||
|
|
||||||
|
**Alternatives considered**:
|
||||||
|
- Fold packaging-ready exports into this feature.
|
||||||
|
- Rejected: broadens scope into report-system territory.
|
||||||
|
- Add BSI, NIS2, CIS, or ISO-specific overlay semantics now.
|
||||||
|
- Rejected: imports a framework engine before the base interpretation path is proven.
|
||||||
349
specs/259-compliance-evidence-mapping/spec.md
Normal file
349
specs/259-compliance-evidence-mapping/spec.md
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
# Feature Specification: Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Feature Branch**: `259-compliance-evidence-mapping`
|
||||||
|
**Created**: 2026-04-30
|
||||||
|
**Status**: Draft
|
||||||
|
**Input**: User description: "Compliance Evidence Mapping v1 as the smallest versioned overlay that maps existing technical governance truth into one customer-safe control/readiness view and one reuse path in the released review or export flow, without certification claims, new control foundations, or a parallel reporting engine."
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: TenantPilot already has repo-real canonical controls, evidence snapshots, findings, accepted-risk governance, and customer-review surfaces, but the product still lacks one explicit customer-safe layer that turns existing technical governance truth into a control/readiness story a customer can understand without operator translation.
|
||||||
|
- **Today's failure**: Operators and customer-facing reviewers still have to translate raw findings, evidence, and accepted-risk details into control meaning by hand. That makes the product harder to sell, easier to overstate, and less trustworthy than the existing repo truth.
|
||||||
|
- **User-visible improvement**: An authorized reviewer can open the existing customer review flow and understand which controls need attention, what evidence supports that view, what risk is accepted, and what next action is recommended without seeing raw diagnostics or certification-style claims.
|
||||||
|
- **Smallest enterprise-capable version**: Add one versioned interpretation overlay over existing canonical control references and render it in the current customer review workspace plus released review detail flow so the same customer-safe control/readiness meaning appears in both surfaces.
|
||||||
|
- **Explicit non-goals**: No certification claims, no legal/compliance guarantees, no BSI/NIS2/CIS/ISO-specific framework engine, no new provider-specific control model, no new portal or panel, no new persistence table, no new report engine, no review authoring or mutation flow, no destructive actions, and no broad export-suite redesign.
|
||||||
|
- **Permanent complexity imported**: One bounded versioned control-interpretation overlay, one shared customer-safe control/readiness vocabulary, one shared summary contract reused across current review surfaces, and focused review/evidence test expansion. No new persisted entity, no new panel, and no multi-framework taxonomy are introduced in v1.
|
||||||
|
- **Why now**: The roadmap and implementation ledger both keep compliance evidence mapping as the next open moat-building follow-up after customer review productization, and `specs/258-customer-review-productization/spec.md` explicitly defers this slice.
|
||||||
|
- **Why not local**: A page-local copy rewrite would not keep workspace and detail flows consistent, would not make interpretation version explicit, and would not provide a reusable customer-safe meaning layer over canonical control references.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: New semantic layer, new versioned interpretation vocabulary, and review-surface reuse concerns. Defense: the slice is bounded to one overlay family and one current review path, derives from existing truth, and explicitly defers framework-specific and packaging follow-ups.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 1 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: canonical-view
|
||||||
|
- **Primary Routes**:
|
||||||
|
- existing admin-plane customer review workspace at `/admin/reviews/workspace`
|
||||||
|
- existing tenant-scoped released review detail route for the selected review
|
||||||
|
- existing evidence summary/proof routes reached from released review context when the actor is entitled
|
||||||
|
- **Data Ownership**: All control/readiness truth remains derived from existing tenant-owned review, finding, accepted-risk, and evidence records plus the existing canonical control catalog. Existing review payloads may carry derived interpretation version and control summary data, but no new workspace-owned or tenant-owned table, projection store, or standalone artifact family is introduced.
|
||||||
|
- **RBAC**:
|
||||||
|
- this remains an admin-plane follow-up, not a new panel or identity surface
|
||||||
|
- workspace membership remains the first isolation boundary
|
||||||
|
- page entry requires an established workspace scope plus at least one entitled tenant the actor may read through the existing capability registry
|
||||||
|
- evidence pointers and deeper proof paths remain capability-gated through current review and evidence authorization paths
|
||||||
|
- non-members or out-of-scope tenant requests resolve as deny-as-not-found
|
||||||
|
- no new customer-only role family or raw capability strings are introduced
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: When launched from tenant-scoped review context, the workspace prefilters to that tenant and foregrounds the latest released review for that tenant. Without incoming tenant context, the page shows only entitled tenants in the current workspace.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Aggregated lists, control summaries, review detail entry, and evidence pointers only resolve for tenants the actor is entitled to in the current workspace. Inaccessible tenant targets are omitted from aggregated lists and resolve as not found when directly targeted.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes
|
||||||
|
- **Interaction class(es)**: status messaging, review summaries, detail disclosure, evidence/report viewers, recommendation phrasing, and safe evidence-link affordances
|
||||||
|
- **Systems touched**: existing `CanonicalControlCatalog`, existing `CanonicalControlResolver`, existing tenant review composition, existing `CustomerReviewWorkspace`, existing released review detail surfaces, and existing evidence-summary/proof presentation
|
||||||
|
- **Existing pattern(s) to extend**: current canonical control references already exposed through tenant review composition and the customer review productization flow from Spec 258
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: `CanonicalControlCatalog`, `CanonicalControlResolver`, `TenantReviewSectionFactory`, `TenantReviewComposer`, and the existing customer review workspace/detail surfaces
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: The repo already has canonical control references flowing into review composition, but it does not yet have one explicit customer-safe interpretation layer over those references. The missing piece is shared interpretation, not new data foundations.
|
||||||
|
- **Allowed deviation and why**: none. The feature must add one shared interpretation path rather than a second page-local control vocabulary.
|
||||||
|
- **Consistency impact**: Control labels, readiness wording, evidence-basis language, recommended action phrasing, accepted-risk influence, and interpretation-version disclosure must stay aligned between workspace summary and released review detail.
|
||||||
|
- **Review focus**: Reviewers must block any page-local control taxonomy, direct framework naming, or raw technical status mapping that bypasses the shared interpretation layer.
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: `N/A`
|
||||||
|
- **Delegated start/completion UX behaviors**: `N/A`
|
||||||
|
- **Local surface-owned behavior that remains**: This slice stays on read-only review and evidence interpretation surfaces and does not add a new run start, queue, resume, or completion path.
|
||||||
|
- **Queued DB-notification policy**: `N/A`
|
||||||
|
- **Terminal notification path**: `N/A`
|
||||||
|
- **Exception required?**: none
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Boundary classification**: platform-core
|
||||||
|
- **Seams affected**: canonical control interpretation, review-summary payloads, customer-safe labels, readiness wording, and evidence-link semantics
|
||||||
|
- **Neutral platform terms preserved or introduced**: canonical control, control readiness, evidence basis, recommendation, interpretation version
|
||||||
|
- **Provider-specific semantics retained and why**: Existing Microsoft subject bindings remain internal resolution inputs only because the current canonical control catalog already depends on them. They must not become customer-visible control language.
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: Customer-facing summaries stay keyed to canonical control keys and released review truth, not provider object IDs, Graph-specific nouns, or framework-specific policy names.
|
||||||
|
- **Follow-up path**: framework-specific overlays and management/export packaging remain follow-up specs after this shared customer-safe layer is stable
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | yes | Native Filament page plus shared review/evidence primitives | status messaging, review summaries, evidence-link affordances | page, table/filter, disclosure state | no | Existing page gains one control/readiness view rather than a new navigation surface |
|
||||||
|
| Released Customer Review detail | yes | Native Filament resource/detail surface plus shared review/evidence primitives | detail disclosure, evidence summaries, recommendation framing | detail sections, disclosure state | no | Existing detail flow becomes the explanation surface for the mapped control/readiness view |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | Primary Decision Surface | Customer reviewer, customer admin, or auditor decides which controls need follow-up and whether the released review is sufficient for the current conversation | control/readiness summary, top evidence basis, accepted-risk influence, and recommended next action | released review detail and entitled evidence pointers after explicit open | Primary because it is the first calm customer-safe review route and should answer the top-level control question without forcing raw technical reconstruction | Follows review-consumption workflow, not storage-object navigation | Replaces manual translation of findings into control meaning across multiple pages |
|
||||||
|
| Released Customer Review detail | Secondary Context Surface | Reader validates why a control is in its current state and what evidence or accepted-risk context supports that interpretation | per-control explanation, evidence basis, accepted-risk context, and next recommendation | deeper proof context only after explicit drilldown and capability checks | Not primary because it deepens the summary chosen in the workspace rather than replacing it | Keeps the workflow centered on one released review after the overview step | Prevents the first page from carrying both high-level decision support and full technical explanation |
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | customer-read-only, customer-admin, auditor-read-only, operator-MSP | which controls need attention, what evidence basis exists, what risk is accepted, and what next step is recommended | release timing and secondary freshness/context only after opening the review | raw payloads, provider IDs, and unrestricted diagnostics stay out of the default path | `Open released review` | raw/support detail and deeper proof context remain hidden or capability-gated | Workspace summary states the mapped control meaning once; the detail surface explains it rather than restating the same summary blocks |
|
||||||
|
| Released Customer Review detail | customer-read-only, customer-admin, auditor-read-only, operator-MSP | per-control readiness meaning, linked findings/evidence basis, accepted-risk influence, interpretation version, and next recommendation | secondary lineage and deeper evidence detail only in follow-on sections | raw payloads, provider-debug data, and unrestricted evidence internals remain hidden or gated | `Review supporting evidence` | support-only detail and deep diagnostics remain outside the default customer-safe view | Detail expands the chosen control state and version; it does not recreate the workspace overview as a separate source of truth |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | List / Table / Read-only workspace report | Read-only registry report | Open the released review for the selected tenant | full-row open to released review detail | required | evidence and proof links stay inside the detail flow, not peer row actions | none | /admin/reviews/workspace | /admin/t/{tenant}/reviews/{record} | workspace context, tenant prefilter, interpretation version, control readiness | Customer review | which controls need attention, why, and what next action is recommended | none |
|
||||||
|
| Released Customer Review detail | Detail / Report / Evidence | Read-only detail report | Review supporting evidence for a surfaced control | sectioned detail page with in-body drilldown | forbidden | in-body evidence pointers only after the mapped control explanation | none | /admin/reviews/workspace | /admin/t/{tenant}/reviews/{record} | workspace, tenant, released review, interpretation version, evidence basis | Customer review | why this control is in its current state and what evidence supports it | none |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace page | Customer reviewer, customer admin, or auditor with read access | Decide which control areas need follow-up conversation and which are sufficiently evidenced for the current review cycle | Read-only workspace review overview | Which control areas matter now, what evidence supports them, and what should happen next? | mapped control states, evidence basis summary, accepted-risk influence, and next recommendation | release lineage, deeper evidence detail, and secondary diagnostics only after drilldown | control readiness, evidence completeness/freshness, accepted-risk influence, release state | none | Open released review | none |
|
||||||
|
| Released Customer Review detail | Customer reviewer, customer admin, or auditor with read access | Understand why a mapped control has its current state and what evidence or accepted-risk context supports that view | Read-only detail report | Why does this control read this way, and what supports or limits that interpretation? | per-control explanation, linked findings, evidence basis, accepted-risk context, interpretation version, and next recommendation | deeper proof metadata and support-only diagnostics | control readiness, evidence sufficiency, accepted-risk timing, interpretation version | none | Review supporting evidence | none |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: yes
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: yes
|
||||||
|
- **New enum/state/reason family?**: yes
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: yes
|
||||||
|
- **Current operator problem**: Existing customer review truth already references canonical controls, but customers still consume technical findings and evidence fragments without one bounded customer-safe control/readiness meaning layer.
|
||||||
|
- **Existing structure is insufficient because**: Raw canonical control references alone do not explain customer-safe readiness meaning, evidence sufficiency, accepted-risk influence, or next action, and page-local copy would drift across workspace and detail surfaces.
|
||||||
|
- **Narrowest correct implementation**: Introduce exactly one versioned interpretation overlay and use it in one current review path so the same mapped control meaning appears in the workspace summary and released review detail without adding a framework engine or new persistence.
|
||||||
|
- **Ownership cost**: Maintain one interpretation catalog/version, one customer-safe readiness vocabulary, one shared summary contract, and focused review/evidence tests that prove consistency and non-overstatement.
|
||||||
|
- **Alternative intentionally rejected**: A copy-only local rewrite was rejected because it would not make the interpretation version explicit or reusable. A multi-framework compliance engine was rejected because it would import broad permanent complexity before a single customer-safe overlay is proven.
|
||||||
|
- **Release truth**: current-release moat and sellability blocker, not future-release preparation
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
|
||||||
|
|
||||||
|
Canonical replacement is preferred over preservation.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Feature, Browser
|
||||||
|
- **Validation lane(s)**: confidence, browser
|
||||||
|
- **Why this classification and these lanes are sufficient**: Focused feature tests are the narrowest sufficient proof for control-reference reuse, customer-safe disclosure, interpretation-version visibility, no-certification wording, tenant isolation, and evidence-link gating. One bounded browser smoke remains justified because the slice materially changes default-visible decision content on an existing review surface.
|
||||||
|
- **New or expanded test families**: expand the existing `Reviews/CustomerReviewWorkspace` and `TenantReview` feature families; keep exactly one bounded browser smoke around the customer review workspace flow
|
||||||
|
- **Fixture / helper cost impact**: low to moderate. Reuse existing tenant review evidence builders, canonical control references, released review fixtures, findings, accepted-risk truth, and evidence snapshot helpers rather than introducing new provider or queue-heavy defaults.
|
||||||
|
- **Heavy-family visibility / justification**: exactly one browser smoke stays explicit because this slice is about customer-safe wording and disclosure on a real rendered page. No broader heavy-governance family is introduced.
|
||||||
|
- **Special surface test profile**: shared-detail-family
|
||||||
|
- **Standard-native relief or required special coverage**: ordinary Filament feature coverage is sufficient for routing, authorization, interpretation consistency, and partial/unmapped states; the existing bounded smoke remains the only required browser proof.
|
||||||
|
- **Reviewer handoff**: Reviewers must confirm that the same control/readiness meaning appears in workspace and detail views, that interpretation version is explicit, that no surface implies certification or legal attestation, that unauthorized tenant targets leak nothing, and that deeper proof remains capability-gated.
|
||||||
|
- **Budget / baseline / trend impact**: low feature-local increase only
|
||||||
|
- **Escalation needed**: none
|
||||||
|
- **Active feature PR close-out entry**: Smoke Coverage
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
- one versioned interpretation overlay over existing canonical control references already present in review composition
|
||||||
|
- customer-safe control/readiness summaries in the existing customer review workspace and released review detail flow
|
||||||
|
- per-control linkage to existing findings, evidence basis, accepted-risk context, and recommended next action
|
||||||
|
- explicit interpretation-version disclosure wherever the mapped control/readiness view appears
|
||||||
|
- explicit separation between technical findings and customer-safe interpretation so the product does not overstate compliance claims
|
||||||
|
- explicit partial, unmapped, stale, unavailable, or accepted-risk-influenced states when the evidence basis does not support a stronger claim
|
||||||
|
- reuse of existing entitlement, redaction, localization, review, evidence, and audit foundations
|
||||||
|
- one shared interpretation contract reused across the current review surfaces so future review/export work can build on the same meaning rather than retranslate it
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- certification claims or legal/regulatory guarantees
|
||||||
|
- hard-coded BSI, NIS2, CIS, ISO, or similar framework semantics in platform core
|
||||||
|
- multiple overlay families, customer profiles, or framework-specific scorecards in v1
|
||||||
|
- a parallel reporting engine, new review-pack format, or recurring governance-package system
|
||||||
|
- new persistence tables, new review publication lifecycle, or a separate compliance artifact family
|
||||||
|
- new panel, portal, customer shell, or identity plane
|
||||||
|
- raw evidence payload viewers, provider-debug views, or support-only diagnostics in the customer-safe default path
|
||||||
|
- remediation, authoring, publication, generation, or other write paths
|
||||||
|
- a new global-searchable resource or widened cross-tenant discovery behavior
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- existing `CanonicalControlCatalog` and `CanonicalControlResolver`
|
||||||
|
- existing canonical control references already surfaced through tenant review composition
|
||||||
|
- existing `TenantReviewSectionFactory` and `TenantReviewComposer`
|
||||||
|
- existing evidence snapshot, finding, and accepted-risk workflow truth
|
||||||
|
- existing `CustomerReviewWorkspace` and released review detail surfaces
|
||||||
|
- customer review productization foundations defined in `specs/258-customer-review-productization/spec.md`
|
||||||
|
- existing workspace and tenant RBAC plus evidence-link capability enforcement
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- Canonical control references already flowing through tenant review composition remain authoritative starting points for the first overlay.
|
||||||
|
- The current Filament v5 and Livewire v4 admin-plane customer review workspace remains the canonical entry surface for v1; no new provider registration or panel path is needed.
|
||||||
|
- One interpretation version identifier can be carried through existing review payloads and surfaces without inventing a new persistence family.
|
||||||
|
- Existing evidence snapshots already contain enough finding and accepted-risk truth to support one customer-safe control/readiness view.
|
||||||
|
- Existing panel assets are sufficient; this slice does not justify new global or on-demand asset registration.
|
||||||
|
- Review-pack packaging and framework-specific overlays can remain follow-up work once one customer-safe interpretation path is proven.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- If the mapped control/readiness wording sounds too definitive, customers could misread the feature as legal attestation rather than product interpretation.
|
||||||
|
- Some released reviews may contain sparse canonical control coverage, forcing partial or unmapped states until more evidence references are present.
|
||||||
|
- If workspace and detail surfaces compute the interpretation separately, the same control could drift between surfaces; the implementation must preserve one shared meaning path.
|
||||||
|
- Over-eager implementation could smuggle in framework naming or export packaging changes because those follow-ups are adjacent; this spec must keep them deferred.
|
||||||
|
- If interpretation version is not visible and traceable, later review history could become harder to defend or compare safely.
|
||||||
|
|
||||||
|
## Candidate Selection Rationale
|
||||||
|
|
||||||
|
- **Selected candidate**: Compliance Evidence Mapping v1
|
||||||
|
- **Source locations**:
|
||||||
|
- `docs/product/spec-candidates.md` active P2 candidate
|
||||||
|
- `docs/product/roadmap.md` compliance moat / executive review follow-through ordering
|
||||||
|
- `docs/product/implementation-ledger.md` open gap `Compliance-oriented control mapping is not productized`
|
||||||
|
- `specs/258-customer-review-productization/spec.md` explicit deferral of this follow-up
|
||||||
|
- existing repo seams around canonical controls, tenant reviews, evidence snapshots, and the customer review workspace
|
||||||
|
- **Why selected**: This is the next unspecced candidate that both roadmap docs and repo truth support after the completed customer-review productization prep. It unlocks a reusable customer-safe interpretation layer that governance packaging depends on.
|
||||||
|
- **Why this is the smallest viable implementation slice**: The repo already has canonical control references, review composition, evidence truth, and the customer review workspace. The missing piece is one bounded customer-safe interpretation overlay over that truth, not a new control foundation or reporting engine.
|
||||||
|
- **Intentional narrowing from source candidate**: This slice deliberately limits itself to one overlay family and the current review surfaces. Multi-framework overlays, management packaging, and broader export presentation remain follow-up work.
|
||||||
|
- **Why close alternatives are deferred**:
|
||||||
|
- Governance-as-a-Service Packaging v1 remains deferred because it depends on this shared interpretation layer before it can package the meaning safely.
|
||||||
|
- External Support Desk / PSA Handoff is a separate commercialization lane and does not unblock the compliance-readiness story.
|
||||||
|
- Private AI Execution Governance Foundation is a later architecture lane and does not address the current customer-safe control/readiness gap.
|
||||||
|
|
||||||
|
## Follow-up Candidates
|
||||||
|
|
||||||
|
- Governance-as-a-Service Packaging v1 once this customer-safe interpretation layer can be reused in one management-ready package
|
||||||
|
- framework-specific overlays only after one shared canonical interpretation path is proven and bounded
|
||||||
|
- review-pack or export reuse of the same interpretation contract once the current review-surface meaning is stable
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Understand control readiness at a glance (Priority: P1)
|
||||||
|
|
||||||
|
An authorized customer reviewer wants the customer review workspace to explain which control areas currently need follow-up, what evidence basis exists, and what next action is recommended without manually translating raw findings.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core moat and sellability gap. If the workspace still reads like raw technical findings, the feature fails.
|
||||||
|
|
||||||
|
**Independent Test**: Sign in as an entitled read-only actor, open the customer review workspace, and confirm that the latest released review for each visible tenant shows a customer-safe control/readiness summary with evidence basis and next recommendation.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an entitled actor has access to a tenant with a released review and canonical control references, **When** they open the customer review workspace, **Then** they see only entitled tenants and a customer-safe control/readiness summary for the current released review.
|
||||||
|
2. **Given** a surfaced control has linked findings, evidence, or accepted-risk context, **When** the actor scans the workspace summary, **Then** they can understand the control state, supporting evidence basis, and recommended next action without opening raw diagnostics.
|
||||||
|
3. **Given** a released review has no mapped canonical control references for the first overlay, **When** the actor opens the workspace, **Then** the surface shows an explicit partial or unmapped state rather than fabricating control coverage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Understand why a control reads this way (Priority: P1)
|
||||||
|
|
||||||
|
An authorized customer reviewer wants the released review detail to explain why a mapped control has its current state so they can trust the interpretation and review the supporting evidence without seeing operator-only residue.
|
||||||
|
|
||||||
|
**Why this priority**: Productization is incomplete if the workspace summary is calm but the first drilldown still forces technical translation.
|
||||||
|
|
||||||
|
**Independent Test**: Open a released review from the workspace and verify that each surfaced control explains linked findings, evidence basis, accepted-risk influence, interpretation version, and next recommendation while keeping raw provider detail hidden by default.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a released review contains mapped control data, **When** the actor opens the released review detail, **Then** they see why each surfaced control has its current state through linked findings, evidence basis, accepted-risk context, and recommendation.
|
||||||
|
2. **Given** deeper proof context exists, **When** the actor stays on the default detail view, **Then** raw payloads, provider IDs, and support-only diagnostics remain hidden until explicit drilldown and capability checks.
|
||||||
|
3. **Given** the evidence basis is incomplete, stale, or partially mapped, **When** the actor reviews the control explanation, **Then** the state is clearly partial or limited rather than overstated as satisfied or compliant.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Trust the interpretation basis and its limits (Priority: P2)
|
||||||
|
|
||||||
|
An authorized customer reviewer wants to see which interpretation version they are reading and to understand that the product is surfacing technical governance truth rather than claiming formal certification.
|
||||||
|
|
||||||
|
**Why this priority**: Versioned interpretation and bounded claims are what make this layer auditable and safe to reuse later.
|
||||||
|
|
||||||
|
**Independent Test**: Open the workspace and released review detail, verify interpretation-version visibility and bounded claim language, and confirm that out-of-scope tenant or gated evidence requests do not leak additional truth.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a mapped control/readiness view is shown, **When** the actor views the workspace or released review detail, **Then** the interpretation version and a bounded non-certification disclosure are visible.
|
||||||
|
2. **Given** the actor targets a tenant outside their scope or a gated evidence link they cannot use, **When** they open that route, **Then** the system reveals no cross-tenant presence and only shows explicit denial or unavailability for in-scope secondary paths.
|
||||||
|
3. **Given** interpretation rules change in a later release, **When** an older released review is inspected, **Then** its surfaced control/readiness view still identifies the interpretation version that produced it.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- What happens when the released review has findings but no canonical control references for the first overlay? The customer-safe surface shows an explicit unmapped or partial state rather than inventing control coverage.
|
||||||
|
- What happens when a control has accepted-risk context that weakens the recommendation? The summary shows the accepted-risk influence and does not present the same state as an unaccepted open issue.
|
||||||
|
- What happens when evidence exists but is stale or incomplete? The control view remains explicit about the limited basis and does not collapse into a stronger readiness claim.
|
||||||
|
- What happens when a user enters through a saved filter or tenant-prefilter for an inaccessible tenant? The filter resolves safely without exposing the tenant or its mapped control state.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature introduces no Microsoft Graph calls, no new write path, no new queue or scheduled work, and no new persistence table. It changes review-surface disclosure, shared interpretation semantics, and existing review payload content.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This follow-up must justify the new interpretation layer as the minimum shared semantic addition needed now, keep it versioned and bounded, and avoid speculative multi-framework modeling or new persistence.
|
||||||
|
|
||||||
|
**Constitution alignment (XCUT-001):** The feature must extend existing canonical control, review, evidence, accepted-risk, localization, and audit paths rather than invent a page-local compliance vocabulary.
|
||||||
|
|
||||||
|
**Constitution alignment (DECIDE-AUD-001 / OPSURF-001):** Default-visible content must remain decision-first and customer-safe, with deeper evidence detail revealed only after explicit user intent.
|
||||||
|
|
||||||
|
**Constitution alignment (TEST-GOV-001):** The implementation must stay with focused feature coverage plus one bounded browser smoke and avoid creating a broader heavy family.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** Workspace and tenant membership remain deny-as-not-found boundaries. Optional secondary evidence access remains capability-gated inside an established scope. No new role strings are introduced by this spec.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001 / UI-NAMING-001 / DECIDE-001 / ACTSURF-001):** The slice must remain a native Filament read-only reporting flow with one dominant inspect action and no destructive or mutation actions.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: The system MUST keep this slice inside the existing admin-plane customer review workspace and released-review detail flow rather than creating a new panel, portal, or customer shell.
|
||||||
|
- **FR-002**: The system MUST derive every mapped control/readiness summary from existing canonical control references, findings, accepted-risk truth, evidence snapshots, and released review truth without creating a new persistence table or a parallel reporting engine.
|
||||||
|
- **FR-003**: The system MUST introduce one explicit versioned interpretation overlay for customer-safe control/readiness meaning and MUST identify that version anywhere the mapped control/readiness view is displayed.
|
||||||
|
- **FR-004**: The default workspace path MUST show only entitled tenants and only released or otherwise customer-safe reviews as the basis for the control/readiness view.
|
||||||
|
- **FR-005**: The default-visible workspace summary MUST answer which control areas need follow-up, what evidence basis exists, whether accepted risk changes the interpretation, and what next action is recommended.
|
||||||
|
- **FR-006**: Mapped control summaries MUST clearly separate technical findings from customer-safe interpretation and MUST NOT claim certification, legal compliance, or framework attestation.
|
||||||
|
- **FR-007**: The released review detail surface MUST explain why each surfaced control has its current state through linked findings, evidence basis, accepted-risk context, and recommendation.
|
||||||
|
- **FR-008**: The feature MUST make unmapped, incomplete, stale, unavailable, and accepted-risk-influenced states explicit and MUST NOT silently collapse them into passing or fully ready states.
|
||||||
|
- **FR-009**: Raw payloads, provider IDs, and support-only diagnostics MUST remain hidden by default and MUST be revealed only through explicit drilldown and existing capability checks.
|
||||||
|
- **FR-010**: The primary action from the workspace MUST remain opening the released review, and any secondary evidence or proof link MUST NOT compete with operator, mutation, or admin-only actions.
|
||||||
|
- **FR-011**: Interpretation labels and readiness wording MUST reuse shared canonical control and review semantics rather than page-local mappings.
|
||||||
|
- **FR-012**: When launched from tenant-scoped context, the workspace MUST preserve a safe tenant prefilter and return path without widening discovery beyond entitled tenants.
|
||||||
|
- **FR-013**: Non-members or out-of-scope workspace or tenant requests MUST resolve as deny-as-not-found, while actors inside an established scope MAY receive explicit capability denial only for gated secondary evidence paths.
|
||||||
|
- **FR-014**: The interpretation version and displayed control/readiness summaries MUST remain traceable through existing review and audit truth so later reviewers can identify which mapping produced a surfaced state.
|
||||||
|
- **FR-015**: The feature MUST NOT introduce a new global-searchable resource or broaden existing search discovery in a way that reveals review, control, or evidence artifacts across tenant boundaries.
|
||||||
|
- **FR-016**: Customer-facing labels and guidance introduced by this slice MUST remain localization-ready for the existing DE/EN product language posture.
|
||||||
|
- **FR-017**: The feature MUST expose no destructive, remediation, authoring, publishing, generation, or admin-only actions in the customer-safe mapped control/readiness path.
|
||||||
|
- **FR-018**: The same mapped control/readiness meaning for a released review MUST remain consistent between workspace summary and released review detail.
|
||||||
|
- **FR-019**: The feature MUST establish one shared interpretation contract reused across the current review surfaces so later review or export work can consume the same meaning without redefining it.
|
||||||
|
- **FR-020**: Framework-specific overlays, management packaging, and certification claims MUST remain explicitly out of scope for this version.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Customer Review Workspace | existing `App\Filament\Pages\Reviews\CustomerReviewWorkspace` | `Clear filters` only | `recordUrl()` or full-row open to the released review | `Open released review` only | none | `Clear filters` only when filters are active; otherwise explanatory no-data text with no create CTA | `N/A` | `N/A` | yes | One primary inspect path keeps the mapped control summary scan-first and avoids parallel evidence-link clutter on the list surface |
|
||||||
|
| Released Customer Review detail | existing tenant-scoped released review detail surface | none by default | `N/A` | `N/A` | none | `N/A` | no new dominant header action; supporting evidence stays in-body and capability-gated | `N/A` | yes | The mapped control/readiness view is explanatory, not mutating; no destructive or generation action is introduced |
|
||||||
|
|
||||||
|
Action Surface Contract is satisfied for this slice. Each affected surface keeps one primary inspect/open model, no empty `ActionGroup` or `BulkActionGroup` placeholder, and no destructive-action placement rules are needed because destructive actions are out of scope. `UI-FIL-001` and `UX-001` are satisfied by staying inside native Filament read-only surfaces, using explicit empty states, and keeping the control/readiness emphasis aligned to shared review semantics rather than page-local visual language.
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Control Interpretation Overlay Version**: The bounded version label that states which customer-safe interpretation rules produced the displayed control/readiness summaries.
|
||||||
|
- **Customer Control Summary**: A derived per-control summary for one released review that combines readiness meaning, evidence basis, accepted-risk influence, and next recommendation without becoming a new persisted entity family.
|
||||||
|
- **Canonical Control Reference**: The existing shared control reference already flowing through tenant review composition and anchoring which controls may be surfaced in the first overlay.
|
||||||
|
- **TenantReview**: The existing released review artifact that anchors the customer-safe review flow and detail context.
|
||||||
|
- **Finding**: The existing issue-level governance truth that feeds the control interpretation and recommendation framing.
|
||||||
|
- **Accepted Risk Decision**: The existing accepted-risk or exception truth that can change how a control state is presented to the customer.
|
||||||
|
- **EvidenceSnapshot**: The existing supporting proof artifact that informs evidence basis summaries and deeper proof drilldown.
|
||||||
|
- **AuditLog**: The existing audit trail used to keep interpretation access and version context traceable without introducing a new audit store.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-001**: An entitled read-only actor can answer which control areas need follow-up, why they read that way, and what next action is recommended from the current customer review flow in two interactions or fewer.
|
||||||
|
- **SC-002**: In 100% of validated customer-safe scenarios, the workspace and released-review detail surfaces show the same mapped control/readiness meaning and interpretation version for the same released review.
|
||||||
|
- **SC-003**: In 100% of validated unauthorized workspace, tenant, or gated-evidence scenarios, the feature reveals no cross-tenant control, review, or evidence presence.
|
||||||
|
- **SC-004**: In 100% of validated mapped-control scenarios, the surface clearly distinguishes technical governance truth from customer-safe interpretation and makes no certification or legal-attestation claim.
|
||||||
|
- **SC-005**: Reviews lacking sufficient mapped control data show an explicit partial or unmapped state rather than falsely implying complete coverage or readiness.
|
||||||
210
specs/259-compliance-evidence-mapping/tasks.md
Normal file
210
specs/259-compliance-evidence-mapping/tasks.md
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
description: "Task list for Compliance Evidence Mapping v1"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Compliance Evidence Mapping v1
|
||||||
|
|
||||||
|
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/`
|
||||||
|
**Prerequisites**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/plan.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/spec.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/contracts/compliance-evidence-mapping.openapi.yaml`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/quickstart.md`
|
||||||
|
|
||||||
|
**Tests**: Required (Pest) for runtime behavior changes. Keep proof in the narrow `confidence` lane plus one bounded `browser` smoke because this slice changes review composition, workspace/detail disclosure, evidence-route reuse, and audit traceability on existing surfaces.
|
||||||
|
**Operations**: No new `OperationRun`, queue, remote call, destructive action, publication flow, generation flow, or background processing is introduced. Auditability stays on the existing shared audit pipeline only.
|
||||||
|
**RBAC**: Workspace membership remains the first boundary. Non-members or out-of-scope tenant targets remain `404`; in-scope actors may receive explicit denial or unavailable messaging only on the reused secondary evidence path. Reuse existing capability registries; do not add raw capability strings or role-string checks.
|
||||||
|
**Filament / Provider Safety**: Filament remains v5 on Livewire v4, panel providers remain registered through `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/bootstrap/providers.php`, no new panel/provider/path or asset strategy is introduced, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php` plus `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` remain globally disabled.
|
||||||
|
**Shared Pattern Reuse**: Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewComposer.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Audit/WorkspaceAuditLogger.php` rather than introducing a second interpretation path, a new report engine, framework-specific overlays, or Governance-as-a-Service packaging scope.
|
||||||
|
**Organization**: Tasks are grouped by user story so shared interpretation composition, workspace rendering, released-review explanation, and evidence-route traceability remain independently testable after the common seams are settled.
|
||||||
|
|
||||||
|
## Test Governance Notes
|
||||||
|
|
||||||
|
- Lane assignment: `confidence` plus one explicit `browser` smoke remain the narrowest sufficient proof for shared interpretation reuse, customer-safe disclosure, tenant isolation, capability-gated evidence drilldown, and interpretation-version traceability.
|
||||||
|
- Keep new coverage inside `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewAuditLogTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspace*.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`; do not widen this slice into a new browser or export/report test family.
|
||||||
|
- Reuse existing released-review, finding, finding-exception, evidence snapshot, entitled-tenant, workspace membership, localization, and audit fixtures; any helper added during implementation must stay explicit and cheap by default.
|
||||||
|
- If implementation finds that current action IDs already cover the required audit moments, close the corresponding audit task as metadata enrichment only and record the outcome as `document-in-feature` instead of creating a new audit event family.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Lock the bounded interpretation overlay scope, validation lanes, and exact repo seams before runtime edits begin.
|
||||||
|
|
||||||
|
- [x] T001 Review the bounded slice, non-goals, guardrail outcomes, and user stories in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/spec.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/plan.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/contracts/compliance-evidence-mapping.openapi.yaml`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/quickstart.md`
|
||||||
|
- [x] T002 [P] Review the shared implementation seams in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Audit/WorkspaceAuditLogger.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/{en,de}/localization.php`
|
||||||
|
- [x] T003 [P] Confirm the focused validation commands and existing proof families in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/quickstart.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Settle the one shared interpretation contract and baseline surface guardrails before any user story-specific rendering work begins.
|
||||||
|
|
||||||
|
**⚠️ CRITICAL**: No user story work should begin until this phase is complete.
|
||||||
|
|
||||||
|
- [x] T004 [P] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php` to lock the bounded `control_interpretation` contract, version-key persistence, limitation flags, and reuse of canonical control references from existing review truth
|
||||||
|
- [x] T005 Create the fixed overlay helper in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php` and wire it to reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlCatalog.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlDefinition.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/CanonicalControlResolver.php` without introducing a second control taxonomy, new persistence table, or framework registry
|
||||||
|
- [x] T006 Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewComposer.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php` to compose one shared summary/detail interpretation payload into the existing `TenantReview` and `TenantReviewSection` JSON only
|
||||||
|
- [x] T007 Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Models/TenantReview.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Models/TenantReviewSection.php` with narrow helpers for the stored interpretation version, summary list, limitation counts, and detail-section access so workspace and detail surfaces read one meaning path
|
||||||
|
- [x] T008 [P] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php` to freeze deny-as-not-found scope handling, read-only customer-workspace posture, unchanged global-search disablement, and the absence of new destructive or authoring actions on the touched surfaces
|
||||||
|
|
||||||
|
**Checkpoint**: The stored interpretation contract, access helpers, and no-scope-creep guardrails are fixed before workspace or detail rendering work begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Understand Control Readiness At A Glance (Priority: P1) 🎯 MVP
|
||||||
|
|
||||||
|
**Goal**: Let an entitled reviewer open the existing customer review workspace and immediately understand which control areas need follow-up, what evidence basis exists, and what next action is recommended.
|
||||||
|
|
||||||
|
**Independent Test**: Open `/admin/reviews/workspace` as an entitled read-only actor and confirm each visible tenant shows only the latest released review, a customer-safe mapped-control summary, explicit limitation states, interpretation version disclosure, and one dominant `Open released review` path.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [x] T009 [P] [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` for visible interpretation version, non-certification disclosure, control summaries, limitation states, evidence-basis wording, recommended next action, explicit partial or unmapped rows, and the truthful page-level empty state when no entitled released review exists
|
||||||
|
- [x] T010 [P] [US1] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` for safe tenant-prefilter launch behavior and one dominant `Open released review` path from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` that keeps the core customer-safe flow within two interactions or fewer
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [x] T011 [US1] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` to build one workspace entry per entitled tenant with a latest released `TenantReview` from its stored `control_interpretation` summary only, while keeping the no-released-review case as a page-level empty state
|
||||||
|
- [x] T012 [US1] Render the mapped-control workspace summary in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` with interpretation version, non-certification disclosure, evidence basis, limitation flags, and no competing primary action
|
||||||
|
- [x] T013 [US1] Keep row-open, tenant-prefilter, and return-path behavior aligned in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php` so the dominant inspect path stays the released-review detail without widening discovery
|
||||||
|
- [x] T014 [US1] Add workspace summary and limitation wording to `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php` using localization-ready customer-safe labels instead of certification or framework-specific language
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 1 is independently functional when the workspace truthfully shows released review summaries for entitled tenants with one dominant inspect path and explicit limitation handling.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Understand Why A Control Reads This Way (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Let the same actor open the released review detail from the workspace and understand the per-control explanation, evidence basis, accepted-risk influence, and recommended next step without seeing operator-only residue.
|
||||||
|
|
||||||
|
**Independent Test**: Open a released review from the workspace and verify that each surfaced control explains its state through stored interpretation payloads, stays read-only in `customer_workspace` mode, and keeps supporting evidence as explicit secondary drilldown only.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [x] T015 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php` for per-control explanation, accepted-risk influence, evidence-basis items, limitation disclosure, and consistency with the stored workspace summary
|
||||||
|
- [x] T016 [P] [US2] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php` for `customer_workspace=1` launch semantics, read-only detail mode, and explanation-first layout with no competing header actions
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [x] T017 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` to read the shared interpretation section from `TenantReviewSection` and keep customer-workspace mode strictly read-only
|
||||||
|
- [x] T018 [US2] Reuse the stored interpretation payload in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` to render per-control explanation text, evidence basis, accepted-risk context, limitation flags, and recommended next action without page-local remapping
|
||||||
|
- [x] T019 [US2] Wire supporting-evidence drilldown through `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` so proof stays an explicit in-body, capability-gated route reuse
|
||||||
|
- [x] T020 [US2] Add released-review explanation and supporting-evidence wording to `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php` so workspace and detail surfaces share one customer-safe vocabulary
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 2 is independently functional when the released review detail deepens the same mapped-control meaning without exposing operator actions, duplicate decision summaries, or raw support detail by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Trust The Interpretation Basis And Its Limits (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Let an entitled reviewer understand which interpretation version they are reading, how that version is traced through audit metadata, and how secondary evidence routes behave without leaking cross-tenant truth.
|
||||||
|
|
||||||
|
**Independent Test**: Open the workspace, released review detail, and an entitled supporting-evidence route; verify interpretation-version continuity, non-certification wording, audit metadata traceability, capability-gated secondary-path behavior, and deny-as-not-found handling for out-of-scope tenant targets.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [x] T021 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/TenantReview/TenantReviewAuditLogTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php` for `interpretation_version`, `source_surface`, `review_id`, and `tenant_filter_id` metadata on released-review and evidence-open events
|
||||||
|
- [x] T022 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` for capability-gated evidence reuse, visible interpretation-version continuity, non-certification wording, and workspace-to-detail drilldown behavior
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [x] T023 [US3] Enrich `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/Audit/WorkspaceAuditLogger.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Audit/AuditActionId.php` metadata handling for `customer_review_workspace.opened`, `tenant_review.opened`, and `evidence_snapshot.opened` without introducing new audit events or stores
|
||||||
|
- Evidence: existing audit events and logger were reused; metadata enrichment is implemented at the existing workspace, review, evidence, and review-pack download call sites, so no new `AuditActionId` value or logger contract change was needed.
|
||||||
|
- [x] T024 [US3] Propagate `source_surface`, `tenant_filter_id`, `review_id`, and `interpretation_version` through `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so workspace, detail, and proof reuse one traceable interpretation path
|
||||||
|
- [x] T025 [US3] Tighten `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so out-of-scope tenant requests stay `404` while in-scope actors get explicit secondary-path denial or unavailability only when capability-gated
|
||||||
|
- [x] T026 [US3] Add version-traceability and non-certification localization keys to `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, keeping Governance-as-a-Service packaging and framework-specific overlays explicitly out of visible copy
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 3 is independently functional when interpretation version and audit traceability stay consistent across workspace, detail, and proof surfaces without widening discovery or implying certification.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Concerns
|
||||||
|
|
||||||
|
**Purpose**: Run the narrow validation set, keep formatting clean, and record bounded reviewer outcomes without widening scope.
|
||||||
|
|
||||||
|
- [x] T027 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- [x] T028 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewAuditLogTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- [x] T029 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||||
|
- [x] T030 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- [x] T031 Record the final `Guardrail / Smoke Coverage` close-out, shared-interpretation-path outcome, audit-metadata reuse outcome, global-search safety outcome, list-surface review outcome, and any `document-in-feature` or `follow-up-spec` decisions in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/plan.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/quickstart.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/259-compliance-evidence-mapping/checklists/requirements.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||||
|
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user stories until the one shared interpretation contract and base guardrails are fixed.
|
||||||
|
- **Phase 3 (US1)**: depends on Phase 2 and delivers the MVP workspace interpretation slice.
|
||||||
|
- **Phase 4 (US2)**: depends on Phase 2 and should follow US1 because the released-review detail must explain the same stored workspace summary on the same shared path.
|
||||||
|
- **Phase 5 (US3)**: depends on Phase 2 and is safest after US1 and US2 because version traceability and evidence-route reuse depend on the shared interpretation already being visible on both surfaces.
|
||||||
|
- **Phase 6 (Polish)**: depends on all implemented stories.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: first independently shippable increment once Phase 2 is complete.
|
||||||
|
- **US2 (P1)**: independently testable after Phase 2, but should merge after US1 because it deepens the same stored interpretation contract on the released-review detail surface.
|
||||||
|
- **US3 (P2)**: independently testable after Phase 2, but should merge after US1 and US2 because audit metadata and evidence-route behavior depend on the shared interpretation being visible end-to-end.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Write the listed Pest coverage first and make it fail for the intended gap before runtime implementation.
|
||||||
|
- Reuse the stored interpretation contract, existing capability checks, and current audit logger before introducing any local mapper, route family, or copy-only duplication.
|
||||||
|
- Re-run the narrowest relevant proof command after each story checkpoint before moving to the next story.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- T002 and T003 can run in parallel after T001 confirms the bounded slice.
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- T004 and T008 can run in parallel while T005 through T007 settle the shared interpretation contract and model-access path.
|
||||||
|
|
||||||
|
### User Story 1
|
||||||
|
|
||||||
|
- T009 and T010 can run in parallel before runtime edits begin.
|
||||||
|
- After T011 settles row composition, T012 and T014 can proceed before T013 finalizes launch and inspect behavior.
|
||||||
|
|
||||||
|
### User Story 2
|
||||||
|
|
||||||
|
- T015 and T016 can run in parallel before detail-surface edits begin.
|
||||||
|
- After T017 lands the read-only detail mode, T018 and T020 can proceed before T019 finalizes secondary evidence drilldown.
|
||||||
|
|
||||||
|
### User Story 3
|
||||||
|
|
||||||
|
- T021 and T022 can run in parallel before audit and proof-path implementation begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Suggested MVP Scope
|
||||||
|
|
||||||
|
- MVP = **Phase 2 + User Story 1** only. That delivers the shared interpretation contract plus the workspace rendering that makes the customer-safe control/readiness overlay visible without yet deepening detail and proof behavior.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Complete Phase 1 and Phase 2.
|
||||||
|
2. Deliver US1 and validate the workspace interpretation contract.
|
||||||
|
3. Deliver US2 and validate the released-review explanation path.
|
||||||
|
4. Deliver US3 and validate audit traceability plus evidence-route reuse.
|
||||||
|
5. Finish with Phase 6 validation, formatting, and reviewer close-out notes.
|
||||||
|
|
||||||
|
### Team Strategy
|
||||||
|
|
||||||
|
1. Settle `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewComposer.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php` first because every surface depends on that stored interpretation payload.
|
||||||
|
2. Parallelize test authoring inside each story before converging on the shared workspace, detail, and evidence files.
|
||||||
|
3. Serialize merges around `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/{en,de}/localization.php` because they are the highest-conflict hotspots for this slice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- This file plans implementation only. No application code is changed by the task-generation step.
|
||||||
|
- The interpretation layer stays bounded to one versioned overlay over existing canonical control references and released review truth.
|
||||||
|
- No new panel/provider, no OperationRun UX, no destructive actions, no new persistence table, no new report engine, no new asset strategy, no global-search expansion, no framework-specific overlay work, and no Governance-as-a-Service packaging work are included in these tasks.
|
||||||
Loading…
Reference in New Issue
Block a user