feat(specs/259): compliance evidence mapping (#312)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m4s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m4s
Implements platform feature branch `259-compliance-evidence-mapping`. Target branch: `platform-dev`. Follow-up integration path after merge: `platform-dev` -> `dev`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #312
This commit is contained in:
parent
0517305381
commit
866875559f
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)
|
||||
- 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)
|
||||
- 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)
|
||||
|
||||
@ -300,9 +302,9 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## 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`
|
||||
- 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 -->
|
||||
|
||||
### Pre-production compatibility check
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\Findings\FindingOutcomeSemantics;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
@ -164,27 +165,26 @@ public function table(Table $table): Table
|
||||
->iconColor(fn (Tenant $record): ?string => $this->latestReviewStateIconColor($record))
|
||||
->description(fn (Tenant $record): ?string => $this->reviewOutcomeDescription($record))
|
||||
->wrap(),
|
||||
TextColumn::make('finding_summary')
|
||||
->label(__('localization.review.key_findings'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->findingSummary($record))
|
||||
TextColumn::make('control_readiness')
|
||||
->label(__('localization.review.control_readiness'))
|
||||
->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(),
|
||||
TextColumn::make('accepted_risk_summary')
|
||||
->label(__('localization.review.accepted_risks'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->acceptedRiskSummary($record))
|
||||
TextColumn::make('evidence_basis')
|
||||
->label(__('localization.review.evidence_basis'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->controlEvidenceBasisSummary($record))
|
||||
->wrap(),
|
||||
TextColumn::make('evidence_proof_state')
|
||||
->label(__('localization.review.evidence_proof'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->evidenceProofAvailability($record))
|
||||
TextColumn::make('recommended_next_action')
|
||||
->label(__('localization.review.recommended_next_action'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->controlRecommendedNextAction($record))
|
||||
->wrap(),
|
||||
TextColumn::make('published_at')
|
||||
->label(__('localization.review.published'))
|
||||
->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString())
|
||||
->dateTime()
|
||||
->placeholder('—'),
|
||||
TextColumn::make('review_pack_state')
|
||||
->label(__('localization.review.review_pack'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->reviewPackAvailability($record))
|
||||
->wrap(),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('tenant_id')
|
||||
@ -206,18 +206,12 @@ public function table(Table $table): Table
|
||||
->icon('heroicon-o-arrow-top-right-on-square')
|
||||
->url(fn (Tenant $record): ?string => $this->latestReviewUrl($record))
|
||||
->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([])
|
||||
->emptyStateHeading(__('localization.review.no_entitled_tenants'))
|
||||
->emptyStateHeading(__('localization.review.no_released_customer_reviews'))
|
||||
->emptyStateDescription(fn (): string => $this->hasActiveFilters()
|
||||
? __('localization.review.clear_filters_description')
|
||||
: __('localization.review.adjust_filters_description'))
|
||||
: __('localization.review.no_released_customer_reviews_description'))
|
||||
->emptyStateActions([
|
||||
Action::make('clear_filters_empty')
|
||||
->label(__('localization.review.clear_filters'))
|
||||
@ -288,6 +282,8 @@ private function auditWorkspaceOpen(): void
|
||||
'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,
|
||||
@ -398,13 +394,20 @@ private function latestReviewUrl(Tenant $tenant): ?string
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->appendQuery(
|
||||
TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant, 'tenant'),
|
||||
$query = array_filter(
|
||||
array_replace(
|
||||
[self::DETAIL_CONTEXT_QUERY_KEY => 1],
|
||||
[
|
||||
'source_surface' => self::SOURCE_SURFACE,
|
||||
'tenant_filter_id' => $this->currentTenantFilterId(),
|
||||
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||
],
|
||||
$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
|
||||
@ -514,6 +517,148 @@ private function reviewOutcomeDescription(Tenant $tenant): ?string
|
||||
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
|
||||
{
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
@ -623,6 +768,50 @@ private function evidenceProofAvailability(Tenant $tenant): string
|
||||
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()
|
||||
|
||||
@ -262,9 +262,7 @@ public static function relatedContextEntries(EvidenceSnapshot $record): array
|
||||
$packUrl = ReviewPackResource::getUrl('view', ['record' => $pack], tenant: $pack->tenant);
|
||||
|
||||
if (static::isCustomerWorkspaceFlow()) {
|
||||
$packUrl = static::appendQuery($packUrl, [
|
||||
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||
]);
|
||||
$packUrl = static::appendQuery($packUrl, static::customerWorkspaceContextQuery());
|
||||
}
|
||||
|
||||
$entries[] = RelatedContextEntry::available(
|
||||
@ -302,6 +300,19 @@ 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
|
||||
*/
|
||||
|
||||
@ -133,6 +133,9 @@ private function auditCustomerWorkspaceProofOpen(): void
|
||||
'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,
|
||||
|
||||
@ -640,6 +640,9 @@ private static function summaryPresentation(TenantReview $record): array
|
||||
$highlights = is_array($summary['highlights'] ?? null) ? $summary['highlights'] : [];
|
||||
$findingOutcomeSummary = static::findingOutcomeSummary($summary);
|
||||
$findingOutcomes = is_array($summary['finding_outcomes'] ?? null) ? $summary['finding_outcomes'] : [];
|
||||
$controlInterpretation = is_array($summary['control_interpretation'] ?? null)
|
||||
? $summary['control_interpretation']
|
||||
: [];
|
||||
|
||||
if ($findingOutcomeSummary !== null) {
|
||||
$highlights[] = __('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.';
|
||||
@ -656,6 +659,7 @@ private static function summaryPresentation(TenantReview $record): array
|
||||
'next_actions' => is_array($summary['recommended_next_actions'] ?? null) ? $summary['recommended_next_actions'] : [],
|
||||
'publish_blockers' => is_array($summary['publish_blockers'] ?? null) ? $summary['publish_blockers'] : [],
|
||||
'context_links' => static::summaryContextLinks($record, static::isCustomerWorkspaceMode()),
|
||||
'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)],
|
||||
@ -713,9 +717,7 @@ private static function summaryContextLinks(TenantReview $record, bool $customer
|
||||
: null;
|
||||
|
||||
if ($customerWorkspaceMode && $evidenceUrl !== null) {
|
||||
$evidenceUrl = static::appendQuery($evidenceUrl, [
|
||||
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||
]);
|
||||
$evidenceUrl = static::appendQuery($evidenceUrl, static::customerWorkspaceEvidenceQuery($record));
|
||||
}
|
||||
|
||||
$links[] = [
|
||||
@ -740,6 +742,24 @@ private static function sectionPresentation(TenantReviewSection $section): array
|
||||
$render = is_array($section->render_payload) ? $section->render_payload : [];
|
||||
$review = $section->tenantReview;
|
||||
$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 [
|
||||
'summary' => collect($summary)->map(function (mixed $value, string $key): ?array {
|
||||
@ -757,7 +777,8 @@ private static function sectionPresentation(TenantReviewSection $section): array
|
||||
'disclosure' => is_string($render['disclosure'] ?? null) ? $render['disclosure'] : null,
|
||||
'next_actions' => is_array($render['next_actions'] ?? null) ? $render['next_actions'] : [],
|
||||
'empty_state' => is_string($render['empty_state'] ?? null) ? $render['empty_state'] : null,
|
||||
'links' => [],
|
||||
'is_control_interpretation' => $section->isControlInterpretation(),
|
||||
'links' => $links,
|
||||
];
|
||||
}
|
||||
|
||||
@ -811,6 +832,19 @@ 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
|
||||
*/
|
||||
|
||||
@ -388,6 +388,9 @@ private function currentReviewPackDownloadUrl(): ?string
|
||||
|
||||
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(),
|
||||
]);
|
||||
}
|
||||
|
||||
@ -444,7 +447,9 @@ private function auditCustomerWorkspaceOpen(): void
|
||||
context: [
|
||||
'metadata' => [
|
||||
'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,
|
||||
|
||||
@ -59,6 +59,9 @@ public function __invoke(Request $request, ReviewPack $reviewPack): StreamedResp
|
||||
? (int) $reviewPack->tenant_review_id
|
||||
: null,
|
||||
'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,
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
namespace App\Models;
|
||||
|
||||
use App\Support\Concerns\DerivesWorkspaceIdFromTenant;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use App\Support\TenantReviewStatus;
|
||||
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)))
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
@ -67,4 +68,26 @@ public function completenessEnum(): TenantReviewCompletenessState
|
||||
return TenantReviewCompletenessState::tryFrom((string) $this->completeness_state)
|
||||
?? 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);
|
||||
$completeness = $this->readinessGate->completenessForSections($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()) {
|
||||
$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'))
|
||||
? 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,
|
||||
'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', []),
|
||||
'recommended_next_actions' => data_get($sections, '0.render_payload.next_actions', []),
|
||||
'last_composed_at' => now()->toIso8601String(),
|
||||
|
||||
@ -81,6 +81,7 @@ public function customerWorkspaceTenantQuery(User $user, Workspace $workspace):
|
||||
return Tenant::query()
|
||||
->where('workspace_id', (int) $workspace->getKey())
|
||||
->whereIn('id', $tenantIds === [] ? [-1] : $tenantIds)
|
||||
->whereHas('tenantReviews', fn ($query) => $query->published())
|
||||
->with([
|
||||
'tenantReviews' => fn ($query) => $query
|
||||
->with(['tenant', 'evidenceSnapshot', 'currentExportReviewPack'])
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\EvidenceSnapshotItem;
|
||||
use App\Support\Findings\FindingOutcomeSemantics;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
@ -15,6 +16,7 @@ final class TenantReviewSectionFactory
|
||||
{
|
||||
public function __construct(
|
||||
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');
|
||||
$operationsItem = $this->item($items, 'operations_summary');
|
||||
|
||||
$controlInterpretation = $this->complianceEvidenceMapping->interpret($snapshot, $findingsItem);
|
||||
|
||||
return [
|
||||
$this->executiveSummarySection($snapshot, $findingsItem, $permissionItem, $rolesItem, $baselineItem, $operationsItem),
|
||||
$controlInterpretation['section'],
|
||||
$this->openRisksSection($findingsItem),
|
||||
$this->acceptedRisksSection($findingsItem),
|
||||
$this->permissionPostureSection($permissionItem, $rolesItem),
|
||||
|
||||
@ -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,10 +132,27 @@
|
||||
'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_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',
|
||||
'clear_filters' => 'Filter löschen',
|
||||
'tenant' => 'Tenant',
|
||||
'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',
|
||||
'accepted_risks' => 'Akzeptierte Risiken',
|
||||
'evidence_proof' => 'Evidence-Nachweis',
|
||||
@ -145,6 +162,8 @@
|
||||
'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_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.',
|
||||
'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',
|
||||
|
||||
@ -132,10 +132,27 @@
|
||||
'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_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',
|
||||
'clear_filters' => 'Clear filters',
|
||||
'tenant' => 'Tenant',
|
||||
'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',
|
||||
'accepted_risks' => 'Accepted risks',
|
||||
'evidence_proof' => 'Evidence proof',
|
||||
@ -145,6 +162,8 @@
|
||||
'download_review_pack' => 'Download review pack',
|
||||
'download_current_review_pack' => 'Download current review pack',
|
||||
'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.',
|
||||
'adjust_filters_description' => 'Adjust filters to return to the full customer review workspace for your entitled tenants.',
|
||||
'no_published_review' => 'No published review',
|
||||
|
||||
@ -9,6 +9,7 @@
|
||||
$links = is_array($state['links'] ?? null) ? $state['links'] : [];
|
||||
$disclosure = is_string($state['disclosure'] ?? null) ? $state['disclosure'] : null;
|
||||
$emptyState = is_string($state['empty_state'] ?? null) ? $state['empty_state'] : null;
|
||||
$isControlInterpretation = (bool) ($state['is_control_interpretation'] ?? false);
|
||||
@endphp
|
||||
|
||||
<div class="space-y-3">
|
||||
@ -48,21 +49,88 @@
|
||||
@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="font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $entry['title'] ?? $entry['displayName'] ?? $entry['type'] ?? __('localization.review.entry') }}
|
||||
</div>
|
||||
@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
|
||||
|
||||
@php
|
||||
$detailParts = collect([
|
||||
$entry['severity'] ?? null,
|
||||
$entry['status'] ?? null,
|
||||
$entry['governance_state'] ?? null,
|
||||
$entry['outcome'] ?? null,
|
||||
])->filter(fn ($value) => is_string($value) && trim($value) !== '')->map(fn (string $value) => \Illuminate\Support\Str::headline($value))->all();
|
||||
@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>
|
||||
|
||||
@if ($detailParts !== [])
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ implode(' · ', $detailParts) }}</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">
|
||||
{{ $entry['title'] ?? $entry['displayName'] ?? $entry['type'] ?? __('localization.review.entry') }}
|
||||
</div>
|
||||
|
||||
@php
|
||||
$detailParts = collect([
|
||||
$entry['severity'] ?? null,
|
||||
$entry['status'] ?? null,
|
||||
$entry['governance_state'] ?? null,
|
||||
$entry['outcome'] ?? null,
|
||||
])->filter(fn ($value) => is_string($value) && trim($value) !== '')->map(fn (string $value) => \Illuminate\Support\Str::headline($value))->all();
|
||||
@endphp
|
||||
|
||||
@if ($detailParts !== [])
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ implode(' · ', $detailParts) }}</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
@ -11,6 +11,13 @@
|
||||
$compressedOutcome = is_array($state['compressed_outcome'] ?? null) ? $state['compressed_outcome'] : [];
|
||||
$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)
|
||||
? trim((string) $compressedOutcome['decisionDirection'])
|
||||
: null;
|
||||
@ -73,6 +80,97 @@
|
||||
@endforeach
|
||||
</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 !== [])
|
||||
<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>
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
<x-filament-panels::page>
|
||||
@php
|
||||
$mappingVersion = \App\Support\Governance\Controls\ComplianceEvidenceMappingV1::VERSION_KEY;
|
||||
@endphp
|
||||
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-3">
|
||||
<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">
|
||||
{{ __('localization.review.customer_workspace_canonical_note') }}
|
||||
</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>
|
||||
</x-filament::section>
|
||||
|
||||
|
||||
@ -85,17 +85,23 @@
|
||||
->waitForText('Customer-safe review workspace')
|
||||
->assertSee('Clear filters')
|
||||
->assertSee('Open latest review')
|
||||
->assertSee('Current review pack available')
|
||||
->assertSee('Proof summary available')
|
||||
->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('Refresh review')
|
||||
->click('Clear filters')
|
||||
->waitForText('No published review available yet')
|
||||
->assertSee('No published review available yet')
|
||||
->waitForText('Published Tenant')
|
||||
->assertDontSee('No Published Tenant')
|
||||
->assertDontSee('No published review available yet')
|
||||
->click('Open latest review')
|
||||
->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('Refresh review')
|
||||
->assertDontSee('Create next review')
|
||||
|
||||
@ -50,6 +50,9 @@
|
||||
$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();
|
||||
|
||||
@ -61,5 +64,8 @@
|
||||
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, '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');
|
||||
});
|
||||
|
||||
@ -451,6 +451,9 @@ function suspendEvidenceSnapshotWorkspace(Tenant $tenant): void
|
||||
$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')
|
||||
@ -462,7 +465,12 @@ function suspendEvidenceSnapshotWorkspace(Tenant $tenant): void
|
||||
$tenant->makeCurrent();
|
||||
Filament::setTenant($tenant, true);
|
||||
|
||||
Livewire::withQueryParams(['source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE])
|
||||
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')
|
||||
|
||||
@ -67,6 +67,9 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
||||
|
||||
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'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();
|
||||
@ -86,6 +89,9 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
||||
->and($audit?->resource_type)->toBe('review_pack')
|
||||
->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, '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);
|
||||
});
|
||||
|
||||
@ -152,7 +152,12 @@
|
||||
|
||||
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)
|
||||
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
||||
->assertSee('Outcome summary')
|
||||
@ -171,5 +176,7 @@
|
||||
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('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());
|
||||
});
|
||||
|
||||
@ -36,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();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
@ -68,11 +68,11 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertTableActionVisible('download_review_pack', $tenant)
|
||||
->assertSee('Current review pack available');
|
||||
->assertDontSee('Download review pack')
|
||||
->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();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
@ -106,11 +106,11 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertTableActionVisible('download_review_pack', $tenant)
|
||||
->assertSee('Current review pack available');
|
||||
->assertDontSee('Download review pack')
|
||||
->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();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
@ -130,11 +130,11 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertTableActionHidden('download_review_pack', $tenant)
|
||||
->assertSee('No current review pack available yet');
|
||||
->assertDontSee('No current review pack available yet')
|
||||
->assertDontSee('Download review pack');
|
||||
});
|
||||
|
||||
it('distinguishes expired and capability-blocked review-pack states on the workspace', 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([
|
||||
@ -173,13 +173,12 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertCanSeeTableRecords([$expiredTenant->fresh(), $blockedTenant->fresh()])
|
||||
->assertSee('Review pack expired')
|
||||
->assertSee('Review pack access is unavailable for this actor')
|
||||
->assertTableActionHidden('download_review_pack', $expiredTenant)
|
||||
->assertTableActionHidden('download_review_pack', $blockedTenant);
|
||||
->assertDontSee('Review pack expired')
|
||||
->assertDontSee('Review pack access is unavailable for this actor')
|
||||
->assertDontSee('Download review pack');
|
||||
});
|
||||
|
||||
it('hides review and pack actions for tenants without a published review', function (): void {
|
||||
it('hides tenants without a published review from the workspace rows', function (): void {
|
||||
$tenant = Tenant::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
@ -198,7 +197,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionHidden('open_latest_review', $tenant)
|
||||
->assertTableActionHidden('download_review_pack', $tenant)
|
||||
->assertSee('No published review available yet');
|
||||
->assertCanNotSeeTableRecords([$tenant->fresh()])
|
||||
->assertSee('No released customer reviews match this view')
|
||||
->assertDontSee('No published review available yet');
|
||||
});
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\FindingException;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\User;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\TenantReviewStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@ -91,6 +92,10 @@
|
||||
->assertCanSeeTableRecords([$tenantA->fresh(), $tenantB->fresh()])
|
||||
->assertCanNotSeeTableRecords([$tenantDenied->fresh()])
|
||||
->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)
|
||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $olderPublishedReview->fresh()], $tenantA), false)
|
||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $newerInternalReview->fresh()], $tenantA), false)
|
||||
@ -102,7 +107,7 @@
|
||||
->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']);
|
||||
[$user, $tenantPublished] = createUserWithTenant(tenant: $tenantPublished, role: 'readonly');
|
||||
|
||||
@ -135,36 +140,46 @@
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertCanSeeTableRecords([$tenantPublished->fresh(), $tenantWithoutPublished->fresh()])
|
||||
->assertSee('No published review')
|
||||
->assertSee('No published review available yet')
|
||||
->assertCanSeeTableRecords([$tenantPublished->fresh()])
|
||||
->assertCanNotSeeTableRecords([$tenantWithoutPublished->fresh()])
|
||||
->assertDontSee('No published review')
|
||||
->assertDontSee('No published review available yet')
|
||||
->assertDontSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $internalOnlyReview->fresh()], $tenantWithoutPublished), false)
|
||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $publishedReview->fresh()], $tenantPublished), false);
|
||||
});
|
||||
|
||||
it('summarizes accepted-risk accountability and evidence proof availability in customer-safe workspace rows', function (): void {
|
||||
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']);
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
|
||||
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => TenantReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
'summary' => array_replace_recursive(is_array($review->summary) ? $review->summary : [], [
|
||||
'risk_acceptance' => [
|
||||
'status_marked_count' => 1,
|
||||
'valid_governed_count' => 1,
|
||||
'warning_count' => 0,
|
||||
],
|
||||
]),
|
||||
])->save();
|
||||
|
||||
$finding = Finding::factory()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||
]);
|
||||
|
||||
FindingException::query()->create([
|
||||
@ -183,6 +198,15 @@
|
||||
'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);
|
||||
@ -190,9 +214,11 @@
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertCanSeeTableRecords([$tenant->fresh()])
|
||||
->assertSee('1 accepted risks are governed. Accountable: Risk Owner. Re-review by')
|
||||
->assertSee('Reason: Vendor patch window accepted by the customer.')
|
||||
->assertSee('Proof summary available');
|
||||
->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 {
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Jobs\GenerateReviewPackJob;
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Filament\Resources\TenantReviewResource;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Services\ReviewPackService;
|
||||
@ -100,3 +102,38 @@
|
||||
->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.');
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
use App\Models\Finding;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
|
||||
it('passes shared canonical control references through tenant review composition', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
@ -11,12 +12,23 @@
|
||||
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||
$openRisks = $review->sections->firstWhere('section_key', 'open_risks');
|
||||
$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)
|
||||
->and($review->canonicalControlReferences()[0]['control_key'])->toBe('endpoint_hardening_compliance')
|
||||
->and($executiveSummary->summary_payload['canonical_control_count'])->toBe(1)
|
||||
->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 {
|
||||
|
||||
@ -84,11 +84,22 @@
|
||||
$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')
|
||||
|
||||
@ -17,12 +17,15 @@
|
||||
|
||||
expect(array_column($payload['sections'], 'section_key'))->toBe([
|
||||
'executive_summary',
|
||||
'control_interpretation',
|
||||
'open_risks',
|
||||
'accepted_risks',
|
||||
'permission_posture',
|
||||
'baseline_drift_posture',
|
||||
'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 {
|
||||
|
||||
@ -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