chore: commit alles (automatisch)
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m44s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m44s
This commit is contained in:
parent
eae06bfe05
commit
7ffdfff054
4
.github/agents/copilot-instructions.md
vendored
4
.github/agents/copilot-instructions.md
vendored
@ -268,6 +268,8 @@ ## Active Technologies
|
||||
- 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, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing `TenantReviewComposer`, `TenantReviewSectionFactory`, `ComplianceEvidenceMappingV1`, `ReviewPackService`, `ArtifactTruthPresenter`, capability helpers, localization copy, and shared audit infrastructure (260-governance-service-packaging)
|
||||
- PostgreSQL via existing `tenant_reviews`, `tenant_review_sections`, `review_packs`, `evidence_snapshots`, `evidence_snapshot_items`, `stored_reports`, `findings`, `finding_exceptions`, `finding_exception_decisions`, memberships, and `audit_logs`; no new persistence planned (260-governance-service-packaging)
|
||||
|
||||
- PHP 8.4.15 (feat/005-bulk-operations)
|
||||
|
||||
@ -302,9 +304,9 @@ ## Code Style
|
||||
PHP 8.4.15: Follow standard conventions
|
||||
|
||||
## Recent Changes
|
||||
- 260-governance-service-packaging: Added PHP 8.4, Laravel 12 + Filament v5, Livewire v4, Pest v4, existing `TenantReviewComposer`, `TenantReviewSectionFactory`, `ComplianceEvidenceMappingV1`, `ReviewPackService`, `ArtifactTruthPresenter`, capability helpers, localization copy, and shared audit infrastructure
|
||||
- 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
|
||||
<!-- MANUAL ADDITIONS START -->
|
||||
|
||||
### Pre-production compatibility check
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
use App\Filament\Resources\TenantReviewResource;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantReview;
|
||||
use App\Models\User;
|
||||
@ -19,6 +20,8 @@
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Governance\Controls\ComplianceEvidenceMappingV1;
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
@ -70,10 +73,10 @@ public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
||||
{
|
||||
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
|
||||
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions provide a single Clear filters action for the customer review workspace.')
|
||||
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
||||
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::PrimaryLinkColumn->value)
|
||||
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The customer review workspace remains scan-first and does not expose bulk actions.')
|
||||
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state keeps exactly one Clear filters CTA when filters are active.')
|
||||
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the latest published review detail instead of an inline canonical detail panel.');
|
||||
->exempt(ActionSurfaceSlot::DetailHeader, 'The dedicated open link column opens the latest published review detail instead of an inline canonical detail panel.');
|
||||
}
|
||||
|
||||
public static function getNavigationGroup(): string
|
||||
@ -150,42 +153,41 @@ public function table(Table $table): Table
|
||||
->persistFiltersInSession()
|
||||
->persistSearchInSession()
|
||||
->persistSortInSession()
|
||||
->recordUrl(fn (Tenant $record): ?string => $this->latestReviewUrl($record))
|
||||
->recordUrl(null)
|
||||
->columns([
|
||||
TextColumn::make('name')->label(__('localization.review.tenant'))->searchable()->sortable(),
|
||||
TextColumn::make('name')->label(__('localization.review.tenant'))->searchable(),
|
||||
TextColumn::make('package_availability')
|
||||
->label(__('localization.review.governance_package'))
|
||||
->width('9rem')
|
||||
->extraHeaderAttributes(['class' => 'whitespace-normal'])
|
||||
->badge()
|
||||
->getStateUsing(fn (Tenant $record): string => $this->governancePackageAvailabilityLabel($record))
|
||||
->color(fn (Tenant $record): string => $this->governancePackageAvailabilityColor($record))
|
||||
->tooltip(fn (Tenant $record): string => $this->governancePackageAvailability($record)['description']),
|
||||
TextColumn::make('latest_review')
|
||||
->label(__('localization.review.latest_review'))
|
||||
->label(__('localization.review.status'))
|
||||
->width('9rem')
|
||||
->badge()
|
||||
->getStateUsing(fn (Tenant $record): string => $this->latestReviewStateLabel($record))
|
||||
->color(fn (Tenant $record): string => $this->latestReviewStateColor($record))
|
||||
->icon(fn (Tenant $record): ?string => $this->latestReviewStateIcon($record))
|
||||
->iconColor(fn (Tenant $record): ?string => $this->latestReviewStateIconColor($record))
|
||||
->description(fn (Tenant $record): ?string => $this->reviewOutcomeDescription($record))
|
||||
->wrap(),
|
||||
TextColumn::make('control_readiness')
|
||||
->label(__('localization.review.control_readiness'))
|
||||
->color(fn (Tenant $record): string => $this->latestReviewStateColor($record)),
|
||||
TextColumn::make('evidence_proof_state')
|
||||
->label(__('localization.review.evidence_status'))
|
||||
->width('8rem')
|
||||
->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('evidence_basis')
|
||||
->label(__('localization.review.evidence_basis'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->controlEvidenceBasisSummary($record))
|
||||
->wrap(),
|
||||
->getStateUsing(fn (Tenant $record): string => $this->evidenceStatusLabel($record))
|
||||
->color(fn (Tenant $record): string => $this->evidenceStatusColor($record)),
|
||||
TextColumn::make('recommended_next_action')
|
||||
->label(__('localization.review.recommended_next_action'))
|
||||
->label(__('localization.review.next_step'))
|
||||
->width('10rem')
|
||||
->extraHeaderAttributes(['class' => 'whitespace-normal'])
|
||||
->getStateUsing(fn (Tenant $record): string => $this->controlRecommendedNextAction($record))
|
||||
->wrap(),
|
||||
TextColumn::make('evidence_proof_state')
|
||||
->label(__('localization.review.evidence_proof'))
|
||||
->getStateUsing(fn (Tenant $record): string => $this->evidenceProofAvailability($record))
|
||||
->wrap(),
|
||||
TextColumn::make('published_at')
|
||||
->label(__('localization.review.published'))
|
||||
->getStateUsing(fn (Tenant $record): ?string => $this->latestPublishedAt($record)?->toDateTimeString())
|
||||
->dateTime()
|
||||
->placeholder('—')
|
||||
TextColumn::make('open_review')
|
||||
->label(__('localization.review.open'))
|
||||
->width('8rem')
|
||||
->getStateUsing(fn (): string => __('localization.review.open_review'))
|
||||
->url(fn (Tenant $record): ?string => $this->latestReviewUrl($record))
|
||||
->color('primary'),
|
||||
])
|
||||
->filters([
|
||||
SelectFilter::make('tenant_id')
|
||||
@ -201,13 +203,7 @@ public function table(Table $table): Table
|
||||
})
|
||||
->searchable(),
|
||||
])
|
||||
->actions([
|
||||
Action::make('open_latest_review')
|
||||
->label(__('localization.review.open_latest_review'))
|
||||
->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),
|
||||
])
|
||||
->actions([])
|
||||
->bulkActions([])
|
||||
->emptyStateHeading(__('localization.review.no_released_customer_reviews'))
|
||||
->emptyStateDescription(fn (): string => $this->hasActiveFilters()
|
||||
@ -401,7 +397,6 @@ private function latestReviewUrl(Tenant $tenant): ?string
|
||||
[
|
||||
'source_surface' => self::SOURCE_SURFACE,
|
||||
'tenant_filter_id' => $this->currentTenantFilterId(),
|
||||
'interpretation_version' => $review->controlInterpretationVersion(),
|
||||
],
|
||||
$this->navigationContext()?->toQuery() ?? [],
|
||||
),
|
||||
@ -441,12 +436,34 @@ private function reviewOutcome(Tenant $tenant): ?CompressedGovernanceOutcome
|
||||
|
||||
private function latestReviewStateLabel(Tenant $tenant): string
|
||||
{
|
||||
return $this->reviewOutcome($tenant)?->primaryLabel ?? __('localization.review.no_published_review');
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return __('localization.review.no_published_review');
|
||||
}
|
||||
|
||||
return $this->workspaceReviewNeedsAttention($tenant)
|
||||
? __('localization.review.review_requires_attention')
|
||||
: __('localization.review.ready_for_release');
|
||||
}
|
||||
|
||||
private function latestReviewStateColor(Tenant $tenant): string
|
||||
{
|
||||
return $this->reviewOutcome($tenant)?->primaryBadge->color ?? 'gray';
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
$packageState = $this->governancePackageAvailability($tenant)['state'];
|
||||
|
||||
if (! $this->workspaceReviewNeedsAttention($tenant)) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return in_array($packageState, ['blocked', 'expired'], true)
|
||||
? 'danger'
|
||||
: 'warning';
|
||||
}
|
||||
|
||||
private function latestReviewStateIcon(Tenant $tenant): ?string
|
||||
@ -499,6 +516,127 @@ private function controlReadinessLabel(Tenant $tenant): string
|
||||
: ComplianceEvidenceMappingV1::readinessLabel((string) ($control['readiness_bucket'] ?? 'review_recommended'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function governancePackageSummary(Tenant $tenant): array
|
||||
{
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$summary = is_array($review->summary) ? $review->summary : [];
|
||||
$package = is_array($summary['governance_package'] ?? null) ? $summary['governance_package'] : [];
|
||||
|
||||
return $package;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{state:string,label:string,description:string}
|
||||
*/
|
||||
private function governancePackageAvailability(Tenant $tenant): array
|
||||
{
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return [
|
||||
'state' => 'unavailable',
|
||||
'label' => __('localization.review.governance_package_unavailable'),
|
||||
'description' => __('localization.review.no_published_review_available'),
|
||||
];
|
||||
}
|
||||
|
||||
$pack = $review->currentExportReviewPack;
|
||||
$user = auth()->user();
|
||||
$limitations = is_array($review->controlInterpretation()['limitations'] ?? null) ? $review->controlInterpretation()['limitations'] : [];
|
||||
$isPartialReview = in_array((string) $review->completeness_state, [
|
||||
TenantReviewCompletenessState::Partial->value,
|
||||
TenantReviewCompletenessState::Stale->value,
|
||||
], true) || $limitations !== [];
|
||||
|
||||
if (! $pack instanceof ReviewPack) {
|
||||
return [
|
||||
'state' => 'unavailable',
|
||||
'label' => __('localization.review.governance_package_unavailable'),
|
||||
'description' => __('localization.review.governance_package_unavailable_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! $user instanceof User || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||
return [
|
||||
'state' => 'blocked',
|
||||
'label' => __('localization.review.governance_package_blocked'),
|
||||
'description' => __('localization.review.governance_package_blocked_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($pack->status === ReviewPackStatus::Expired->value || ($pack->expires_at !== null && $pack->expires_at->isPast())) {
|
||||
return [
|
||||
'state' => 'expired',
|
||||
'label' => __('localization.review.governance_package_expired'),
|
||||
'description' => __('localization.review.governance_package_expired_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return [
|
||||
'state' => 'unavailable',
|
||||
'label' => __('localization.review.governance_package_unavailable'),
|
||||
'description' => __('localization.review.governance_package_not_ready_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPartialReview) {
|
||||
return [
|
||||
'state' => 'partial',
|
||||
'label' => __('localization.review.governance_package_partial'),
|
||||
'description' => __('localization.review.governance_package_partial_description'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => 'available',
|
||||
'label' => __('localization.review.governance_package_available'),
|
||||
'description' => __('localization.review.governance_package_available_description'),
|
||||
];
|
||||
}
|
||||
|
||||
private function governancePackageAvailabilityLabel(Tenant $tenant): string
|
||||
{
|
||||
return match ($this->governancePackageAvailability($tenant)['state']) {
|
||||
'available' => __('localization.review.available'),
|
||||
'partial' => __('localization.review.partial'),
|
||||
'blocked' => __('localization.review.blocked'),
|
||||
'expired' => __('localization.review.expired'),
|
||||
default => __('localization.review.unavailable'),
|
||||
};
|
||||
}
|
||||
|
||||
private function governancePackageAvailabilityColor(Tenant $tenant): string
|
||||
{
|
||||
return match ($this->governancePackageAvailability($tenant)['state']) {
|
||||
'available' => 'success',
|
||||
'partial' => 'warning',
|
||||
'blocked', 'expired' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
private function governancePackageTeaser(Tenant $tenant): string
|
||||
{
|
||||
$package = $this->governancePackageSummary($tenant);
|
||||
|
||||
$executiveSummary = $package['executive_summary'] ?? null;
|
||||
|
||||
if (is_string($executiveSummary) && trim($executiveSummary) !== '') {
|
||||
return $executiveSummary;
|
||||
}
|
||||
|
||||
return $this->governancePackageAvailability($tenant)['description'];
|
||||
}
|
||||
|
||||
private function controlReadinessColor(Tenant $tenant): string
|
||||
{
|
||||
return match ((string) ($this->primaryControlSummary($tenant)['readiness_bucket'] ?? 'unmapped')) {
|
||||
@ -518,16 +656,9 @@ private function controlReadinessDescription(Tenant $tenant): string
|
||||
}
|
||||
|
||||
$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');
|
||||
return __('localization.review.control_readiness_unmapped_description');
|
||||
}
|
||||
|
||||
$summary = collect($controls)
|
||||
@ -550,7 +681,7 @@ private function controlReadinessDescription(Tenant $tenant): string
|
||||
|
||||
$limitations = $this->controlLimitationSummary($review);
|
||||
|
||||
return trim($prefix.$summary.($limitations !== null ? ' '.$limitations : ''));
|
||||
return trim($summary.($limitations !== null ? ' '.$limitations : ''));
|
||||
}
|
||||
|
||||
private function controlEvidenceBasisSummary(Tenant $tenant): string
|
||||
@ -569,6 +700,86 @@ private function controlEvidenceBasisSummary(Tenant $tenant): string
|
||||
}
|
||||
|
||||
private function controlRecommendedNextAction(Tenant $tenant): string
|
||||
{
|
||||
if ($this->primaryControlSummary($tenant) === null) {
|
||||
return __('localization.review.workspace_next_step_control_mapping');
|
||||
}
|
||||
|
||||
if ($this->evidenceStatusState($tenant) !== 'available') {
|
||||
return __('localization.review.workspace_next_step_evidence_review');
|
||||
}
|
||||
|
||||
return match ($this->governancePackageAvailability($tenant)['state']) {
|
||||
'available', 'partial' => __('localization.review.workspace_next_step_package_review'),
|
||||
default => __('localization.review.workspace_next_step_review_open'),
|
||||
};
|
||||
}
|
||||
|
||||
private function workspaceReviewNeedsAttention(Tenant $tenant): bool
|
||||
{
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->primaryControlSummary($tenant) === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->evidenceStatusState($tenant) !== 'available') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $this->governancePackageAvailability($tenant)['state'] !== 'available';
|
||||
}
|
||||
|
||||
private function evidenceStatusState(Tenant $tenant): string
|
||||
{
|
||||
$review = $this->latestPublishedReview($tenant);
|
||||
|
||||
if (! $review instanceof TenantReview) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
$snapshot = $review->evidenceSnapshot;
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $snapshot instanceof EvidenceSnapshot) {
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
if (! $user instanceof User || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
||||
return 'restricted';
|
||||
}
|
||||
|
||||
if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
||||
return 'expired';
|
||||
}
|
||||
|
||||
return 'available';
|
||||
}
|
||||
|
||||
private function evidenceStatusLabelForState(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
'available' => __('localization.review.available'),
|
||||
'restricted' => __('localization.review.restricted'),
|
||||
'expired' => __('localization.review.expired'),
|
||||
default => __('localization.review.pending'),
|
||||
};
|
||||
}
|
||||
|
||||
private function evidenceStatusColorForState(string $state): string
|
||||
{
|
||||
return match ($state) {
|
||||
'available' => 'success',
|
||||
'restricted', 'expired' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
}
|
||||
|
||||
private function controlRecommendedNextActionDescription(Tenant $tenant): string
|
||||
{
|
||||
$control = $this->primaryControlSummary($tenant);
|
||||
|
||||
@ -707,6 +918,16 @@ private function evidenceProofAvailability(Tenant $tenant): string
|
||||
return __('localization.review.evidence_proof_available');
|
||||
}
|
||||
|
||||
private function evidenceStatusLabel(Tenant $tenant): string
|
||||
{
|
||||
return $this->evidenceStatusLabelForState($this->evidenceStatusState($tenant));
|
||||
}
|
||||
|
||||
private function evidenceStatusColor(Tenant $tenant): string
|
||||
{
|
||||
return $this->evidenceStatusColorForState($this->evidenceStatusState($tenant));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
use App\Filament\Resources\TenantReviewResource\Pages;
|
||||
use App\Exceptions\Entitlements\WorkspaceEntitlementBlockedException;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantReview;
|
||||
use App\Models\TenantReviewSection;
|
||||
@ -26,6 +27,7 @@
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\OpsUx\OperationUxPresenter;
|
||||
use App\Support\ReasonTranslation\ReasonPresenter;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\Rbac\UiEnforcement;
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use App\Support\TenantReviewStatus;
|
||||
@ -234,6 +236,7 @@ public static function infolist(Schema $schema): Schema
|
||||
Section::make(__('localization.review.sections'))
|
||||
->schema([
|
||||
RepeatableEntry::make('sections')
|
||||
->state(fn (TenantReview $record): array => static::visibleSections($record))
|
||||
->hiddenLabel()
|
||||
->schema([
|
||||
TextEntry::make('title'),
|
||||
@ -263,6 +266,17 @@ public static function infolist(Schema $schema): Schema
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, TenantReviewSection>
|
||||
*/
|
||||
private static function visibleSections(TenantReview $record): array
|
||||
{
|
||||
return $record->sections
|
||||
->reject(fn (TenantReviewSection $section): bool => static::isCustomerWorkspaceMode() && $section->isControlInterpretation())
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public static function table(Table $table): Table
|
||||
{
|
||||
$exportExecutivePackAction = UiEnforcement::forTableAction(
|
||||
@ -643,6 +657,7 @@ private static function summaryPresentation(TenantReview $record): array
|
||||
$controlInterpretation = is_array($summary['control_interpretation'] ?? null)
|
||||
? $summary['control_interpretation']
|
||||
: [];
|
||||
$packagePresentation = static::governancePackagePresentation($record);
|
||||
|
||||
if ($findingOutcomeSummary !== null) {
|
||||
$highlights[] = __('localization.review.terminal_outcomes').': '.$findingOutcomeSummary.'.';
|
||||
@ -660,11 +675,8 @@ private static function summaryPresentation(TenantReview $record): array
|
||||
'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)],
|
||||
['label' => __('localization.review.verified_cleared'), 'value' => (string) ($findingOutcomes[FindingOutcomeSemantics::OUTCOME_VERIFIED_CLEARED] ?? 0)],
|
||||
] : [
|
||||
'governance_package' => $packagePresentation,
|
||||
'metrics' => static::isCustomerWorkspaceMode() ? static::customerWorkspaceMetrics($record, $summary, $packagePresentation) : [
|
||||
['label' => __('localization.review.findings'), 'value' => (string) ($summary['finding_count'] ?? 0)],
|
||||
['label' => __('localization.review.reports'), 'value' => (string) ($summary['report_count'] ?? 0)],
|
||||
['label' => __('localization.review.operations'), 'value' => (string) ($summary['operation_count'] ?? 0)],
|
||||
@ -675,6 +687,157 @@ private static function summaryPresentation(TenantReview $record): array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $summary
|
||||
* @param array<string, mixed> $packagePresentation
|
||||
* @return array<int, array{label:string,value:string}>
|
||||
*/
|
||||
private static function customerWorkspaceMetrics(TenantReview $record, array $summary, array $packagePresentation): array
|
||||
{
|
||||
$acceptedRisk = is_array($summary['risk_acceptance'] ?? null) ? $summary['risk_acceptance'] : [];
|
||||
|
||||
return [
|
||||
['label' => __('localization.review.governance_package'), 'value' => (string) ($packagePresentation['availability']['label'] ?? __('localization.review.governance_package_unavailable'))],
|
||||
['label' => __('localization.review.review_status'), 'value' => static::customerReviewStatusLabel($record)],
|
||||
['label' => __('localization.review.evidence_status'), 'value' => static::customerEvidenceStatusLabel($record)],
|
||||
['label' => __('localization.review.accepted_risk_status'), 'value' => static::customerAcceptedRiskStatusLabel($acceptedRisk)],
|
||||
['label' => __('localization.review.last_review'), 'value' => $record->published_at?->format('Y-m-d') ?? __('localization.review.pending')],
|
||||
];
|
||||
}
|
||||
|
||||
private static function customerReviewStatusLabel(TenantReview $record): string
|
||||
{
|
||||
if ($record->isPublished() && (string) $record->completeness_state === TenantReviewCompletenessState::Complete->value) {
|
||||
return __('localization.review.review_completed');
|
||||
}
|
||||
|
||||
if ($record->isPublished()) {
|
||||
return __('localization.review.review_requires_attention');
|
||||
}
|
||||
|
||||
return Str::headline((string) $record->status);
|
||||
}
|
||||
|
||||
private static function customerEvidenceStatusLabel(TenantReview $record): string
|
||||
{
|
||||
$snapshot = $record->evidenceSnapshot;
|
||||
$tenant = $record->tenant;
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $snapshot instanceof EvidenceSnapshot) {
|
||||
return __('localization.review.evidence_pending');
|
||||
}
|
||||
|
||||
if (! $user instanceof User || ! $tenant instanceof Tenant || ! $user->can(Capabilities::EVIDENCE_VIEW, $tenant)) {
|
||||
return __('localization.review.evidence_restricted');
|
||||
}
|
||||
|
||||
if ((string) $snapshot->status === 'expired' || ($snapshot->expires_at !== null && $snapshot->expires_at->isPast())) {
|
||||
return __('localization.review.evidence_expired');
|
||||
}
|
||||
|
||||
return __('localization.review.evidence_available');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $acceptedRisk
|
||||
*/
|
||||
private static function customerAcceptedRiskStatusLabel(array $acceptedRisk): string
|
||||
{
|
||||
$warningCount = (int) ($acceptedRisk['warning_count'] ?? 0);
|
||||
$statusMarkedCount = (int) ($acceptedRisk['status_marked_count'] ?? 0);
|
||||
|
||||
if ($warningCount > 0) {
|
||||
return __('localization.review.accepted_risk_follow_up');
|
||||
}
|
||||
|
||||
if ($statusMarkedCount > 0) {
|
||||
return __('localization.review.accepted_risk_on_record', ['count' => $statusMarkedCount]);
|
||||
}
|
||||
|
||||
return __('localization.review.accepted_risk_none');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private static function governancePackagePresentation(TenantReview $record): array
|
||||
{
|
||||
$summary = is_array($record->summary) ? $record->summary : [];
|
||||
$package = is_array($summary['governance_package'] ?? null) ? $summary['governance_package'] : [];
|
||||
|
||||
if ($package === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_merge($package, [
|
||||
'availability' => static::governancePackageAvailability($record),
|
||||
'delivery_note' => __('localization.review.governance_package_delivery_note'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{state:string,label:string,description:string}
|
||||
*/
|
||||
private static function governancePackageAvailability(TenantReview $record): array
|
||||
{
|
||||
$pack = $record->currentExportReviewPack;
|
||||
$tenant = $record->tenant;
|
||||
$user = auth()->user();
|
||||
$controlInterpretation = $record->controlInterpretation();
|
||||
$limitations = is_array($controlInterpretation['limitations'] ?? null) ? $controlInterpretation['limitations'] : [];
|
||||
$isPartialReview = in_array((string) $record->completeness_state, [
|
||||
TenantReviewCompletenessState::Partial->value,
|
||||
TenantReviewCompletenessState::Stale->value,
|
||||
], true) || $limitations !== [];
|
||||
|
||||
if (! $pack instanceof ReviewPack) {
|
||||
return [
|
||||
'state' => 'unavailable',
|
||||
'label' => __('localization.review.governance_package_unavailable'),
|
||||
'description' => __('localization.review.governance_package_unavailable_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if (! $user instanceof User || ! $tenant instanceof Tenant || ! $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant)) {
|
||||
return [
|
||||
'state' => 'blocked',
|
||||
'label' => __('localization.review.governance_package_blocked'),
|
||||
'description' => __('localization.review.governance_package_blocked_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($pack->status === ReviewPackStatus::Expired->value || ($pack->expires_at !== null && $pack->expires_at->isPast())) {
|
||||
return [
|
||||
'state' => 'expired',
|
||||
'label' => __('localization.review.governance_package_expired'),
|
||||
'description' => __('localization.review.governance_package_expired_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return [
|
||||
'state' => 'unavailable',
|
||||
'label' => __('localization.review.governance_package_unavailable'),
|
||||
'description' => __('localization.review.governance_package_not_ready_description'),
|
||||
];
|
||||
}
|
||||
|
||||
if ($isPartialReview) {
|
||||
return [
|
||||
'state' => 'partial',
|
||||
'label' => __('localization.review.governance_package_partial'),
|
||||
'description' => __('localization.review.governance_package_partial_description'),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => 'available',
|
||||
'label' => __('localization.review.governance_package_available'),
|
||||
'description' => __('localization.review.governance_package_available_description'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array{title:string,label:string,url:?string,description:string}>
|
||||
*/
|
||||
|
||||
@ -355,7 +355,7 @@ private function archiveReviewAction(): Actions\Action
|
||||
private function downloadCurrentReviewPackAction(): Actions\Action
|
||||
{
|
||||
return Actions\Action::make('download_current_review_pack')
|
||||
->label(__('localization.review.download_current_review_pack'))
|
||||
->label(__('localization.review.download_governance_package'))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('primary')
|
||||
->disabled(fn (): bool => $this->currentReviewPackDownloadUrl() === null)
|
||||
|
||||
@ -38,8 +38,14 @@ public function compose(EvidenceSnapshot $snapshot, ?TenantReview $review = null
|
||||
$sectionStateCounts = $this->readinessGate->sectionStateCounts($sections);
|
||||
$completeness = $this->readinessGate->completenessForSections($sections);
|
||||
$status = $this->readinessGate->statusForSections($sections);
|
||||
$executiveSummarySection = collect($sections)
|
||||
->firstWhere('section_key', 'executive_summary');
|
||||
$controlInterpretationSection = collect($sections)
|
||||
->firstWhere('section_key', 'control_interpretation');
|
||||
$openRisksSection = collect($sections)
|
||||
->firstWhere('section_key', 'open_risks');
|
||||
$acceptedRisksSection = collect($sections)
|
||||
->firstWhere('section_key', 'accepted_risks');
|
||||
$operationsSection = collect($sections)
|
||||
->firstWhere('section_key', 'operations_health');
|
||||
|
||||
@ -86,9 +92,246 @@ public function compose(EvidenceSnapshot $snapshot, ?TenantReview $review = null
|
||||
'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', []),
|
||||
'governance_package' => $this->governancePackageSummary(
|
||||
snapshot: $snapshot,
|
||||
executiveSummarySection: is_array($executiveSummarySection) ? $executiveSummarySection : [],
|
||||
controlInterpretationSection: is_array($controlInterpretationSection) ? $controlInterpretationSection : [],
|
||||
openRisksSection: is_array($openRisksSection) ? $openRisksSection : [],
|
||||
acceptedRisksSection: is_array($acceptedRisksSection) ? $acceptedRisksSection : [],
|
||||
),
|
||||
'last_composed_at' => now()->toIso8601String(),
|
||||
],
|
||||
'sections' => $sections,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $executiveSummarySection
|
||||
* @param array<string, mixed> $controlInterpretationSection
|
||||
* @param array<string, mixed> $openRisksSection
|
||||
* @param array<string, mixed> $acceptedRisksSection
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function governancePackageSummary(
|
||||
EvidenceSnapshot $snapshot,
|
||||
array $executiveSummarySection,
|
||||
array $controlInterpretationSection,
|
||||
array $openRisksSection,
|
||||
array $acceptedRisksSection,
|
||||
): array {
|
||||
$executiveSummaryPayload = is_array($executiveSummarySection['summary_payload'] ?? null)
|
||||
? $executiveSummarySection['summary_payload']
|
||||
: [];
|
||||
$executiveRenderPayload = is_array($executiveSummarySection['render_payload'] ?? null)
|
||||
? $executiveSummarySection['render_payload']
|
||||
: [];
|
||||
$controlInterpretationSummary = is_array($controlInterpretationSection['summary_payload'] ?? null)
|
||||
? $controlInterpretationSection['summary_payload']
|
||||
: [];
|
||||
$openRiskEntries = collect(data_get($openRisksSection, 'render_payload.entries', []))
|
||||
->filter(static fn (mixed $entry): bool => is_array($entry))
|
||||
->take(3)
|
||||
->map(fn (array $entry): array => $this->packageFindingEntry($entry))
|
||||
->values()
|
||||
->all();
|
||||
$acceptedRiskEntries = collect(data_get($acceptedRisksSection, 'render_payload.entries', []))
|
||||
->filter(static fn (mixed $entry): bool => is_array($entry))
|
||||
->map(fn (array $entry): array => $this->packageAcceptedRiskEntry($entry))
|
||||
->values();
|
||||
$governanceDecisionEntries = $acceptedRiskEntries
|
||||
->filter(fn (array $entry): bool => $this->requiresGovernanceDecisionFollowUp($entry))
|
||||
->values();
|
||||
$stableAcceptedRiskEntries = $acceptedRiskEntries
|
||||
->reject(fn (array $entry): bool => $this->requiresGovernanceDecisionFollowUp($entry))
|
||||
->values();
|
||||
$governanceDecisions = $governanceDecisionEntries
|
||||
->map(fn (array $entry): array => $this->packageGovernanceDecisionEntry($entry))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
return [
|
||||
'delivery_artifact_family' => 'review_pack',
|
||||
'interpretation_version' => is_string($controlInterpretationSummary['version_key'] ?? null)
|
||||
? $controlInterpretationSummary['version_key']
|
||||
: null,
|
||||
'executive_summary' => $this->governancePackageExecutiveSummary(
|
||||
executiveSummaryPayload: $executiveSummaryPayload,
|
||||
executiveRenderPayload: $executiveRenderPayload,
|
||||
controlInterpretationSummary: $controlInterpretationSummary,
|
||||
acceptedRiskCount: $acceptedRiskEntries->count(),
|
||||
),
|
||||
'top_findings' => $openRiskEntries,
|
||||
'accepted_risks' => $stableAcceptedRiskEntries->all(),
|
||||
'governance_decisions' => $governanceDecisions,
|
||||
'evidence_basis_summary' => $this->governancePackageEvidenceBasisSummary(
|
||||
snapshot: $snapshot,
|
||||
controlInterpretationSummary: $controlInterpretationSummary,
|
||||
),
|
||||
'supporting_artifact_links' => [
|
||||
[
|
||||
'artifact_family' => 'evidence_snapshot',
|
||||
'artifact_key' => 'evidence_snapshot:'.$snapshot->getKey(),
|
||||
'purpose' => 'evidence_basis',
|
||||
],
|
||||
[
|
||||
'artifact_family' => 'review_pack',
|
||||
'artifact_key' => 'review_pack:current_export',
|
||||
'purpose' => 'stakeholder_delivery',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function packageFindingEntry(array $entry): array
|
||||
{
|
||||
return [
|
||||
'finding_id' => is_numeric($entry['id'] ?? null) ? (int) $entry['id'] : null,
|
||||
'title' => $this->entryTitle($entry, 'Open finding'),
|
||||
'severity' => is_string($entry['severity'] ?? null) ? $entry['severity'] : 'unknown',
|
||||
'status' => is_string($entry['status'] ?? null) ? $entry['status'] : 'unknown',
|
||||
'summary' => $this->entrySummary($entry, 'This finding remains open in the released review and should be discussed in stakeholder delivery.'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function packageAcceptedRiskEntry(array $entry): array
|
||||
{
|
||||
return [
|
||||
'finding_id' => is_numeric($entry['id'] ?? null) ? (int) $entry['id'] : null,
|
||||
'title' => $this->entryTitle($entry, 'Accepted risk'),
|
||||
'governance_state' => is_string($entry['governance_state'] ?? null) ? $entry['governance_state'] : 'unknown',
|
||||
'summary' => $this->entrySummary($entry, 'This accepted-risk entry qualifies the current governance position for stakeholder delivery.'),
|
||||
'owner_label' => $this->ownerLabel($entry),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
*/
|
||||
private function requiresGovernanceDecisionFollowUp(array $entry): bool
|
||||
{
|
||||
return in_array((string) ($entry['governance_state'] ?? ''), [
|
||||
'expired_exception',
|
||||
'revoked_exception',
|
||||
'risk_accepted_without_valid_exception',
|
||||
], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function packageGovernanceDecisionEntry(array $entry): array
|
||||
{
|
||||
$governanceState = (string) ($entry['governance_state'] ?? 'unknown');
|
||||
|
||||
return [
|
||||
'finding_id' => $entry['finding_id'] ?? null,
|
||||
'title' => $entry['title'] ?? 'Governance decision',
|
||||
'governance_state' => $governanceState,
|
||||
'summary' => match ($governanceState) {
|
||||
'expired_exception' => 'The accepted-risk exception has expired and needs follow-up before stakeholder delivery.',
|
||||
'revoked_exception' => 'The accepted-risk exception was revoked and needs follow-up before stakeholder delivery.',
|
||||
'risk_accepted_without_valid_exception' => 'The accepted-risk entry has no currently valid exception basis and needs follow-up before stakeholder delivery.',
|
||||
default => 'This governance decision needs follow-up before stakeholder delivery.',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $executiveSummaryPayload
|
||||
* @param array<string, mixed> $executiveRenderPayload
|
||||
* @param array<string, mixed> $controlInterpretationSummary
|
||||
*/
|
||||
private function governancePackageExecutiveSummary(
|
||||
array $executiveSummaryPayload,
|
||||
array $executiveRenderPayload,
|
||||
array $controlInterpretationSummary,
|
||||
int $acceptedRiskCount,
|
||||
): string {
|
||||
$highlights = collect($executiveRenderPayload['highlights'] ?? [])
|
||||
->filter(static fn (mixed $highlight): bool => is_string($highlight) && trim($highlight) !== '')
|
||||
->values();
|
||||
|
||||
if ($highlights->isNotEmpty()) {
|
||||
return (string) $highlights->first();
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'This released review summarizes %d mapped control(s), %d open risk(s), and %d accepted-risk item(s) from the anchored evidence basis.',
|
||||
(int) ($controlInterpretationSummary['mapped_control_count'] ?? 0),
|
||||
(int) ($executiveSummaryPayload['open_risk_count'] ?? 0),
|
||||
$acceptedRiskCount,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $controlInterpretationSummary
|
||||
*/
|
||||
private function governancePackageEvidenceBasisSummary(EvidenceSnapshot $snapshot, array $controlInterpretationSummary): string
|
||||
{
|
||||
return sprintf(
|
||||
'Anchored to evidence snapshot #%d with %s completeness and %d mapped control(s).',
|
||||
(int) $snapshot->getKey(),
|
||||
(string) $snapshot->completeness_state,
|
||||
(int) ($controlInterpretationSummary['mapped_control_count'] ?? 0),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
*/
|
||||
private function entryTitle(array $entry, string $fallback): string
|
||||
{
|
||||
foreach (['title', 'name', 'finding_title'] as $key) {
|
||||
$value = $entry[$key] ?? null;
|
||||
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
*/
|
||||
private function entrySummary(array $entry, string $fallback): string
|
||||
{
|
||||
foreach (['customer_summary', 'summary', 'request_reason'] as $key) {
|
||||
$value = $entry[$key] ?? null;
|
||||
|
||||
if (is_string($value) && trim($value) !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $entry
|
||||
*/
|
||||
private function ownerLabel(array $entry): ?string
|
||||
{
|
||||
$owner = $entry['owner'] ?? null;
|
||||
|
||||
if (is_array($owner)) {
|
||||
$name = $owner['name'] ?? null;
|
||||
|
||||
if (is_string($name) && trim($name) !== '') {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -129,18 +129,21 @@
|
||||
'reporting' => 'Berichte',
|
||||
'customer_reviews' => 'Kundenreviews',
|
||||
'customer_review_workspace' => 'Kundenreview-Workspace',
|
||||
'customer_safe_review_workspace' => 'Kundensicherer Review-Workspace',
|
||||
'customer_workspace_intro' => 'Prüfen Sie den zuletzt veröffentlichten kundensicheren Status für jeden berechtigten Tenant, ohne den aktuellen Workspace-Kontext zu verlassen.',
|
||||
'customer_workspace_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_safe_review_workspace' => 'Kundensicherer Governance-Paket-Index',
|
||||
'customer_workspace_intro' => 'Prüfen Sie für jeden berechtigten Tenant den aktuellen Status des Governance-Pakets und öffnen Sie bei Bedarf die kundensichere Detailansicht.',
|
||||
'customer_workspace_canonical_note' => 'Jede Zeile ist ein Einstieg in die Detailansicht: Dort sehen Sie Paketstatus, Nachweise, aktuelle Risiken und den nächsten kundensicheren Schritt.',
|
||||
'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.',
|
||||
'customer_workspace_non_certification_disclosure' => 'Dieser Workspace fasst die aktuelle Review- und Nachweislage für die Service-Auslieferung zusammen. Er ersetzt weder ein formales Auditurteil noch eine Zertifizierung oder rechtliche Attestierung.',
|
||||
'reviews' => 'Reviews',
|
||||
'clear_filters' => 'Filter löschen',
|
||||
'tenant' => 'Tenant',
|
||||
'latest_review' => 'Letztes Review',
|
||||
'review_status' => 'Review-Status',
|
||||
'status' => 'Status',
|
||||
'control' => 'Control',
|
||||
'control_interpretation' => 'Control-Readiness-Interpretation',
|
||||
'control_readiness' => 'Control-Readiness',
|
||||
'assessment_status' => 'Prüfstatus',
|
||||
'review_recommended' => 'Review empfohlen',
|
||||
'recommended_next_action' => 'Empfohlene nächste Aktion',
|
||||
'customer_safe' => 'Kundensicher',
|
||||
@ -156,11 +159,31 @@
|
||||
'key_findings' => 'Wichtige Findings',
|
||||
'accepted_risks' => 'Akzeptierte Risiken',
|
||||
'evidence_proof' => 'Evidence-Nachweis',
|
||||
'evidence_status' => 'Nachweise',
|
||||
'published' => 'Veröffentlicht',
|
||||
'review_pack' => 'Review-Pack',
|
||||
'open_latest_review' => 'Letztes Review öffnen',
|
||||
'open' => 'Öffnen',
|
||||
'open_review' => 'Review öffnen',
|
||||
'last_review' => 'Letztes Review',
|
||||
'primary_action' => 'Primäre Aktion',
|
||||
'download_review_pack' => 'Review-Pack herunterladen',
|
||||
'download_current_review_pack' => 'Aktuelles Review-Pack herunterladen',
|
||||
'download_governance_package' => 'Governance-Paket herunterladen',
|
||||
'governance_package' => 'Governance-Paket',
|
||||
'governance_decisions' => 'Governance-Entscheidungen',
|
||||
'governance_package_delivery_note' => 'Dieses Governance-Paket wird über das aktuelle Export-Review-Pack des veröffentlichten Reviews ausgeliefert.',
|
||||
'governance_package_available' => 'Governance-Paket verfügbar',
|
||||
'governance_package_available_description' => 'Das aktuelle Export-Review-Pack ist aus diesem veröffentlichten Review für die Stakeholder-Auslieferung bereit.',
|
||||
'governance_package_partial' => 'Governance-Paket partiell',
|
||||
'governance_package_partial_description' => 'Das aktuelle Export-Review-Pack ist bereit, aber die zugrunde liegende Review-Basis bleibt partiell oder limitierungsbehaftet.',
|
||||
'governance_package_unavailable' => 'Governance-Paket nicht verfügbar',
|
||||
'governance_package_unavailable_description' => 'Diesem veröffentlichten Review ist noch kein aktuelles Export-Review-Pack zugeordnet.',
|
||||
'governance_package_not_ready_description' => 'Das aktuelle Export-Review-Pack ist für die Stakeholder-Auslieferung noch nicht bereit.',
|
||||
'governance_package_expired' => 'Governance-Paket abgelaufen',
|
||||
'governance_package_expired_description' => 'Das aktuelle Export-Review-Pack ist abgelaufen und kann aus diesem veröffentlichten Review nicht heruntergeladen werden.',
|
||||
'governance_package_blocked' => 'Governance-Paket blockiert',
|
||||
'governance_package_blocked_description' => 'Dieses Konto kann das veröffentlichte Review lesen, aber das aktuelle Export-Review-Pack nicht 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.',
|
||||
@ -181,6 +204,10 @@
|
||||
'accepted_risk_partial_accountability' => 'Die Verantwortlichkeit ist teilweise erfasst; Review-Owner-Details sind nicht vollständig verfügbar.',
|
||||
'unavailable' => 'Nicht verfügbar',
|
||||
'available' => 'Verfügbar',
|
||||
'partial' => 'Teilweise',
|
||||
'blocked' => 'Blockiert',
|
||||
'expired' => 'Abgelaufen',
|
||||
'restricted' => 'Eingeschränkt',
|
||||
'review_pack_available' => 'Aktuelles Review-Pack verfügbar',
|
||||
'no_current_review_pack' => 'Noch kein aktuelles Review-Pack verfügbar',
|
||||
'review_pack_access_unavailable' => 'Review-Pack-Zugriff ist für dieses Konto nicht verfügbar',
|
||||
@ -190,6 +217,19 @@
|
||||
'evidence_proof_absent' => 'Noch keine Nachweiszusammenfassung verknüpft',
|
||||
'evidence_proof_access_unavailable' => 'Nachweiszugriff ist für dieses Konto nicht verfügbar',
|
||||
'evidence_proof_expired' => 'Nachweiszusammenfassung abgelaufen',
|
||||
'evidence_available' => 'Nachweise verfügbar',
|
||||
'evidence_pending' => 'Nachweise ausstehend',
|
||||
'evidence_restricted' => 'Nachweise eingeschränkt',
|
||||
'evidence_expired' => 'Nachweise abgelaufen',
|
||||
'assessment_basis' => 'Prüfgrundlage',
|
||||
'assessment_basis_description' => 'Diese Prüfbereiche zeigen, wie die Aussagen des Pakets durch die aktuelle Review-Evidenz gestützt werden.',
|
||||
'review_completed' => 'Review abgeschlossen',
|
||||
'review_requires_attention' => 'Prüfung erforderlich',
|
||||
'ready_for_release' => 'Zur Veröffentlichung bereit',
|
||||
'accepted_risk_status' => 'Status akzeptierter Risiken',
|
||||
'accepted_risk_none' => 'Keine erfasst',
|
||||
'accepted_risk_on_record' => ':count erfasst',
|
||||
'accepted_risk_follow_up' => 'Nacharbeit erforderlich',
|
||||
'customer_review_pack_unavailable' => 'Das aktuelle Review-Pack kann aus diesem kundensicheren Flow nicht heruntergeladen werden.',
|
||||
'customer_review_pack_missing' => 'Diesem veröffentlichten Review ist noch kein aktuelles Review-Pack zugeordnet.',
|
||||
'customer_review_pack_not_ready' => 'Das zugeordnete Review-Pack ist noch nicht für den Download bereit.',
|
||||
@ -210,6 +250,10 @@
|
||||
'outcome' => 'Ergebnis',
|
||||
'export' => 'Export',
|
||||
'next_step' => 'Nächster Schritt',
|
||||
'workspace_next_step_evidence_review' => 'Nachweise prüfen',
|
||||
'workspace_next_step_review_open' => 'Review öffnen',
|
||||
'workspace_next_step_package_review' => 'Paket prüfen',
|
||||
'workspace_next_step_control_mapping' => 'Kontrollzuordnung prüfen',
|
||||
'no_tenant_reviews_yet' => 'Noch keine Tenant-Reviews',
|
||||
'create_first_review_description' => 'Erstellen Sie das erste Review aus einem verankerten Evidence-Snapshot, um die wiederkehrende Review-Historie für diesen Tenant zu starten.',
|
||||
'create_first_review' => 'Erstes Review erstellen',
|
||||
|
||||
@ -129,18 +129,21 @@
|
||||
'reporting' => 'Reporting',
|
||||
'customer_reviews' => 'Customer reviews',
|
||||
'customer_review_workspace' => 'Customer Review Workspace',
|
||||
'customer_safe_review_workspace' => 'Customer-safe review workspace',
|
||||
'customer_workspace_intro' => 'Review the latest published customer-safe posture for each entitled tenant without leaving the current workspace context.',
|
||||
'customer_workspace_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_safe_review_workspace' => 'Customer-safe governance package index',
|
||||
'customer_workspace_intro' => 'Review the current governance package status for each entitled tenant and open the customer-safe detail when follow-up is needed.',
|
||||
'customer_workspace_canonical_note' => 'Each row is an index entry: open the review detail to inspect package status, supporting evidence, current risks, and the next customer-safe action.',
|
||||
'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.',
|
||||
'customer_workspace_non_certification_disclosure' => 'This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.',
|
||||
'reviews' => 'Reviews',
|
||||
'clear_filters' => 'Clear filters',
|
||||
'tenant' => 'Tenant',
|
||||
'latest_review' => 'Latest review',
|
||||
'review_status' => 'Review status',
|
||||
'status' => 'Status',
|
||||
'control' => 'Control',
|
||||
'control_interpretation' => 'Control readiness interpretation',
|
||||
'control_readiness' => 'Control readiness',
|
||||
'assessment_status' => 'Assessment status',
|
||||
'review_recommended' => 'Review recommended',
|
||||
'recommended_next_action' => 'Recommended next action',
|
||||
'customer_safe' => 'Customer-safe',
|
||||
@ -156,11 +159,31 @@
|
||||
'key_findings' => 'Key findings',
|
||||
'accepted_risks' => 'Accepted risks',
|
||||
'evidence_proof' => 'Evidence proof',
|
||||
'evidence_status' => 'Evidence',
|
||||
'published' => 'Published',
|
||||
'review_pack' => 'Review pack',
|
||||
'open_latest_review' => 'Open latest review',
|
||||
'open' => 'Open',
|
||||
'open_review' => 'Open review',
|
||||
'last_review' => 'Last review',
|
||||
'primary_action' => 'Primary action',
|
||||
'download_review_pack' => 'Download review pack',
|
||||
'download_current_review_pack' => 'Download current review pack',
|
||||
'download_governance_package' => 'Download governance package',
|
||||
'governance_package' => 'Governance package',
|
||||
'governance_decisions' => 'Governance decisions',
|
||||
'governance_package_delivery_note' => 'This governance package is delivered through the current export review pack for the released review.',
|
||||
'governance_package_available' => 'Governance package available',
|
||||
'governance_package_available_description' => 'The current export review pack is ready for stakeholder delivery from this released review.',
|
||||
'governance_package_partial' => 'Governance package partial',
|
||||
'governance_package_partial_description' => 'The current export review pack is ready, but the supporting review basis remains partial or limitation-aware.',
|
||||
'governance_package_unavailable' => 'Governance package unavailable',
|
||||
'governance_package_unavailable_description' => 'No current export review pack is attached to this released review yet.',
|
||||
'governance_package_not_ready_description' => 'The current export review pack is not ready for stakeholder delivery yet.',
|
||||
'governance_package_expired' => 'Governance package expired',
|
||||
'governance_package_expired_description' => 'The current export review pack has expired and cannot be downloaded from this released review.',
|
||||
'governance_package_blocked' => 'Governance package blocked',
|
||||
'governance_package_blocked_description' => 'This account can read the released review but cannot download the current export 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.',
|
||||
@ -181,6 +204,10 @@
|
||||
'accepted_risk_partial_accountability' => 'Accountability is partially recorded; review owner details are not fully available.',
|
||||
'unavailable' => 'Unavailable',
|
||||
'available' => 'Available',
|
||||
'partial' => 'Partial',
|
||||
'blocked' => 'Blocked',
|
||||
'expired' => 'Expired',
|
||||
'restricted' => 'Restricted',
|
||||
'review_pack_available' => 'Current review pack available',
|
||||
'no_current_review_pack' => 'No current review pack available yet',
|
||||
'review_pack_access_unavailable' => 'Review pack access is unavailable for this actor',
|
||||
@ -190,6 +217,19 @@
|
||||
'evidence_proof_absent' => 'No proof summary linked yet',
|
||||
'evidence_proof_access_unavailable' => 'Proof access is unavailable for this actor',
|
||||
'evidence_proof_expired' => 'Proof summary expired',
|
||||
'evidence_available' => 'Evidence available',
|
||||
'evidence_pending' => 'Evidence pending',
|
||||
'evidence_restricted' => 'Evidence restricted',
|
||||
'evidence_expired' => 'Evidence expired',
|
||||
'assessment_basis' => 'Assessment basis',
|
||||
'assessment_basis_description' => 'These assessment areas explain how the package statements are supported by the current review evidence.',
|
||||
'review_completed' => 'Review completed',
|
||||
'review_requires_attention' => 'Review required',
|
||||
'ready_for_release' => 'Ready for release',
|
||||
'accepted_risk_status' => 'Accepted risk status',
|
||||
'accepted_risk_none' => 'None on record',
|
||||
'accepted_risk_on_record' => ':count on record',
|
||||
'accepted_risk_follow_up' => 'Follow-up required',
|
||||
'customer_review_pack_unavailable' => 'The current review pack cannot be downloaded from this customer-safe flow.',
|
||||
'customer_review_pack_missing' => 'No current review pack is attached to this released review yet.',
|
||||
'customer_review_pack_not_ready' => 'The attached review pack is not ready for download yet.',
|
||||
@ -210,6 +250,10 @@
|
||||
'outcome' => 'Outcome',
|
||||
'export' => 'Export',
|
||||
'next_step' => 'Next step',
|
||||
'workspace_next_step_evidence_review' => 'Review evidence',
|
||||
'workspace_next_step_review_open' => 'Open review',
|
||||
'workspace_next_step_package_review' => 'Review package',
|
||||
'workspace_next_step_control_mapping' => 'Review control mapping',
|
||||
'no_tenant_reviews_yet' => 'No tenant reviews yet',
|
||||
'create_first_review_description' => 'Create the first review from an anchored evidence snapshot to start the recurring review history for this tenant.',
|
||||
'create_first_review' => 'Create first review',
|
||||
|
||||
@ -12,6 +12,11 @@
|
||||
$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'] : [];
|
||||
$governancePackage = is_array($state['governance_package'] ?? null) ? $state['governance_package'] : [];
|
||||
$packageAvailability = is_array($governancePackage['availability'] ?? null) ? $governancePackage['availability'] : [];
|
||||
$packageTopFindings = is_array($governancePackage['top_findings'] ?? null) ? $governancePackage['top_findings'] : [];
|
||||
$packageAcceptedRisks = is_array($governancePackage['accepted_risks'] ?? null) ? $governancePackage['accepted_risks'] : [];
|
||||
$packageGovernanceDecisions = is_array($governancePackage['governance_decisions'] ?? null) ? $governancePackage['governance_decisions'] : [];
|
||||
$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)
|
||||
@ -27,6 +32,19 @@
|
||||
$publicationReason = is_string($compressedOutcome['primaryReason'] ?? null) && trim((string) $compressedOutcome['primaryReason']) !== ''
|
||||
? trim((string) $compressedOutcome['primaryReason'])
|
||||
: null;
|
||||
$packageNextStep = $publicationNextAction;
|
||||
if ($packageNextStep === null) {
|
||||
$firstNextAction = $nextActions[0] ?? null;
|
||||
$packageNextStep = is_string($firstNextAction) && trim($firstNextAction) !== '' ? $firstNextAction : null;
|
||||
}
|
||||
$assessmentControls = array_slice($controlControls, 0, 2);
|
||||
$additionalAssessmentControls = max(count($controlControls) - count($assessmentControls), 0);
|
||||
$packageAvailabilityColor = match ($packageAvailability['state'] ?? 'gray') {
|
||||
'available' => 'success',
|
||||
'partial' => 'warning',
|
||||
'blocked', 'expired' => 'danger',
|
||||
default => 'gray',
|
||||
};
|
||||
@endphp
|
||||
|
||||
<div class="space-y-4">
|
||||
@ -80,19 +98,162 @@
|
||||
@endforeach
|
||||
</dl>
|
||||
|
||||
@if ($customerWorkspaceMode && $governancePackage !== [])
|
||||
<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">
|
||||
{{ __('localization.review.governance_package') }}
|
||||
</div>
|
||||
|
||||
@if (filled($governancePackage['delivery_note'] ?? null))
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ $governancePackage['delivery_note'] }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($packageAvailability !== [])
|
||||
<x-filament::badge :color="$packageAvailabilityColor" size="sm">
|
||||
{{ $packageAvailability['label'] ?? __('localization.review.unavailable') }}
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.primary_action') }}</div>
|
||||
<div class="mt-1 text-sm font-medium text-gray-900 dark:text-gray-100">{{ __('localization.review.download_governance_package') }}</div>
|
||||
</div>
|
||||
|
||||
@if ($packageNextStep !== null)
|
||||
<div class="rounded-md border border-primary-100 bg-primary-50 px-3 py-2 dark:border-primary-900/40 dark:bg-primary-950/30">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-primary-700 dark:text-primary-200">{{ __('localization.review.next_step') }}</div>
|
||||
<div class="mt-1 text-sm text-primary-900 dark:text-primary-100">{{ $packageNextStep }}</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if (filled($governancePackage['executive_summary'] ?? null))
|
||||
<div class="text-sm text-gray-700 dark:text-gray-300">
|
||||
{{ $governancePackage['executive_summary'] }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (filled($governancePackage['evidence_basis_summary'] ?? null))
|
||||
<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">
|
||||
<div class="text-[11px] font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.evidence_basis') }}</div>
|
||||
<div class="mt-1">{{ $governancePackage['evidence_basis_summary'] }}</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($packageAvailability !== [] && filled($packageAvailability['description'] ?? null))
|
||||
<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">
|
||||
{{ $packageAvailability['description'] }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($packageTopFindings !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.key_findings') }}</div>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($packageTopFindings as $finding)
|
||||
@php
|
||||
$findingTitle = is_string($finding['title'] ?? null) ? $finding['title'] : __('localization.review.control');
|
||||
$findingSummary = is_string($finding['summary'] ?? null) ? $finding['summary'] : null;
|
||||
@endphp
|
||||
|
||||
<li class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">{{ $findingTitle }}</div>
|
||||
@if ($findingSummary !== null && trim($findingSummary) !== '')
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $findingSummary }}</div>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($packageAcceptedRisks !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.accepted_risks') }}</div>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($packageAcceptedRisks as $risk)
|
||||
@php
|
||||
$riskTitle = is_string($risk['title'] ?? null) ? $risk['title'] : __('localization.review.accepted_risks');
|
||||
$riskSummary = is_string($risk['summary'] ?? null) ? $risk['summary'] : null;
|
||||
@endphp
|
||||
|
||||
<li class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">
|
||||
<div class="font-medium text-gray-900 dark:text-gray-100">{{ $riskTitle }}</div>
|
||||
@if ($riskSummary !== null && trim($riskSummary) !== '')
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $riskSummary }}</div>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($packageGovernanceDecisions !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.governance_decisions') }}</div>
|
||||
<ul class="space-y-1 text-sm text-amber-800 dark:text-amber-200">
|
||||
@foreach ($packageGovernanceDecisions as $decision)
|
||||
@php
|
||||
$decisionTitle = is_string($decision['title'] ?? null) ? $decision['title'] : __('localization.review.governance_decisions');
|
||||
$decisionSummary = is_string($decision['summary'] ?? null) ? $decision['summary'] : null;
|
||||
@endphp
|
||||
|
||||
<li class="rounded-md border border-amber-100 bg-amber-50 px-3 py-2 dark:border-amber-900/40 dark:bg-amber-950/30">
|
||||
<div class="font-medium">{{ $decisionTitle }}</div>
|
||||
@if ($decisionSummary !== null && trim($decisionSummary) !== '')
|
||||
<div class="mt-1 text-xs">{{ $decisionSummary }}</div>
|
||||
@endif
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</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>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($highlights as $highlight)
|
||||
@continue(! is_string($highlight) || trim($highlight) === '')
|
||||
|
||||
<li class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">{{ $highlight }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($nextActions !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.next_actions') }}</div>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($nextActions as $action)
|
||||
@continue(! is_string($action) || trim($action) === '')
|
||||
|
||||
<li class="rounded-md border border-primary-100 bg-primary-50 px-3 py-2 dark:border-primary-900/40 dark:bg-primary-950/30">{{ $action }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@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') }}
|
||||
{{ __('localization.review.assessment_basis') }}
|
||||
</div>
|
||||
<div class="mt-1 text-sm text-gray-600 dark:text-gray-300">
|
||||
{{ __('localization.review.assessment_basis_description') }}
|
||||
</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">
|
||||
@ -106,9 +267,9 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($controlControls !== [])
|
||||
@if ($assessmentControls !== [])
|
||||
<div class="space-y-2">
|
||||
@foreach ($controlControls as $control)
|
||||
@foreach ($assessmentControls as $control)
|
||||
@php
|
||||
$readinessBucket = is_string($control['readiness_bucket'] ?? null) ? $control['readiness_bucket'] : 'review_recommended';
|
||||
$readinessColor = match ($readinessBucket) {
|
||||
@ -143,6 +304,12 @@
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if (filled($control['recommended_next_action'] ?? null))
|
||||
<div class="mt-3 rounded-md border border-primary-100 bg-primary-50 px-3 py-2 text-sm text-primary-900 dark:border-primary-900/40 dark:bg-primary-950/30 dark:text-primary-100">
|
||||
{{ $control['recommended_next_action'] }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($limitationLabels !== [])
|
||||
<div class="mt-2 flex flex-wrap gap-1">
|
||||
@foreach ($limitationLabels as $label)
|
||||
@ -157,6 +324,12 @@
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
@if ($additionalAssessmentControls > 0)
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ __('localization.review.additional_controls', ['count' => $additionalAssessmentControls]) }}
|
||||
</div>
|
||||
@endif
|
||||
@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') }}
|
||||
@ -171,32 +344,6 @@
|
||||
</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>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($highlights as $highlight)
|
||||
@continue(! is_string($highlight) || trim($highlight) === '')
|
||||
|
||||
<li class="rounded-md border border-gray-100 bg-gray-50 px-3 py-2 dark:border-gray-800 dark:bg-gray-950/60">{{ $highlight }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($nextActions !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.next_actions') }}</div>
|
||||
<ul class="space-y-1 text-sm text-gray-700 dark:text-gray-300">
|
||||
@foreach ($nextActions as $action)
|
||||
@continue(! is_string($action) || trim($action) === '')
|
||||
|
||||
<li class="rounded-md border border-primary-100 bg-primary-50 px-3 py-2 dark:border-primary-900/40 dark:bg-primary-950/30">{{ $action }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($contextLinks !== [])
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">{{ __('localization.review.related_context') }}</div>
|
||||
|
||||
@ -1,8 +1,4 @@
|
||||
<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">
|
||||
@ -17,10 +13,6 @@
|
||||
{{ __('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>
|
||||
|
||||
@ -82,26 +82,36 @@
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->click('Open customer workspace')
|
||||
->waitForText('Customer-safe review workspace')
|
||||
->waitForText('Customer-safe governance package index')
|
||||
->assertSee('Clear filters')
|
||||
->assertSee('Open latest review')
|
||||
->assertSee('Control readiness')
|
||||
->assertSee('Endpoint hardening and compliance')
|
||||
->assertSee('Compliance evidence mapping v1')
|
||||
->assertSee('This is not a certification, legal attestation, or compliance guarantee.')
|
||||
->assertSee('Follow-up required')
|
||||
->assertSee('Open review')
|
||||
->assertSee('Governance package')
|
||||
->assertSee('Status')
|
||||
->assertSee('Evidence')
|
||||
->assertSee('This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.')
|
||||
->assertSee('Partial')
|
||||
->assertSee('Review required')
|
||||
->assertSee('Available')
|
||||
->assertSee('Review package')
|
||||
->assertDontSee('Publishable')
|
||||
->assertDontSee('No mapped controls')
|
||||
->assertDontSee('Compliance evidence mapping v1')
|
||||
->assertDontSee('Publish review')
|
||||
->assertDontSee('Refresh review')
|
||||
->click('Clear filters')
|
||||
->waitForText('Published Tenant')
|
||||
->assertDontSee('No Published Tenant')
|
||||
->assertDontSee('No published review available yet')
|
||||
->click('Open latest review')
|
||||
->click('Open review')
|
||||
->waitForText('Outcome summary')
|
||||
->assertSee('Download current review pack')
|
||||
->assertSee('Download governance package')
|
||||
->assertSee('Governance package')
|
||||
->assertSee('Released governance record')
|
||||
->assertSee('Control readiness interpretation')
|
||||
->assertSee('Compliance evidence mapping v1')
|
||||
->assertSee('Review status')
|
||||
->assertSee('Primary action')
|
||||
->assertSee('Assessment basis')
|
||||
->assertDontSee('Control readiness interpretation')
|
||||
->assertDontSee('Compliance evidence mapping v1')
|
||||
->assertDontSee('Publish review')
|
||||
->assertDontSee('Refresh review')
|
||||
->assertDontSee('Create next review')
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
||||
use App\Filament\Resources\TenantReviewResource;
|
||||
use App\Models\PlatformUser;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\Tenant;
|
||||
@ -67,7 +68,9 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false)
|
||||
->assertSee('Governance package')
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('Current review pack available');
|
||||
});
|
||||
@ -105,7 +108,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false)
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Download review pack')
|
||||
->assertDontSee('Current review pack available');
|
||||
});
|
||||
@ -129,12 +133,49 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertTableActionVisible('open_latest_review', $tenant)
|
||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false)
|
||||
->assertSee('Unavailable')
|
||||
->assertDontSee('No current review pack available yet')
|
||||
->assertDontSee('Download review pack');
|
||||
});
|
||||
|
||||
it('keeps expired and capability-blocked review-pack states off the workspace row surface', function (): void {
|
||||
it('shows a partial governance-package state when the released review basis is limitation-aware', function (): void {
|
||||
$tenant = Tenant::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedPartialTenantReviewEvidence($tenant);
|
||||
|
||||
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => TenantReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'tenant_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'tenant_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
])->save();
|
||||
|
||||
$this->actingAs($user);
|
||||
setAdminPanelContext();
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertSee(TenantReviewResource::tenantScopedUrl('view', ['record' => $review->fresh()], $tenant), false)
|
||||
->assertSee('Partial')
|
||||
->assertDontSee('Download review pack');
|
||||
});
|
||||
|
||||
it('shows expired and capability-blocked governance-package states on 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,8 +214,8 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(Tenant $tenant): void
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertCanSeeTableRecords([$expiredTenant->fresh(), $blockedTenant->fresh()])
|
||||
->assertDontSee('Review pack expired')
|
||||
->assertDontSee('Review pack access is unavailable for this actor')
|
||||
->assertSee('Expired')
|
||||
->assertSee('Blocked')
|
||||
->assertDontSee('Download review pack');
|
||||
});
|
||||
|
||||
|
||||
@ -92,10 +92,19 @@
|
||||
->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('Review the current governance package status for each entitled tenant and open the customer-safe detail when follow-up is needed.')
|
||||
->assertSee('This workspace summarizes current review evidence for service delivery. It does not replace a formal audit opinion, certification, or legal attestation.')
|
||||
->assertSee('Governance package')
|
||||
->assertSee('Status')
|
||||
->assertSee('Evidence')
|
||||
->assertSee('Next step')
|
||||
->assertSee('Open')
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Assessment status')
|
||||
->assertDontSee('Publishable')
|
||||
->assertDontSee('No mapped controls')
|
||||
->assertDontSee('Compliance evidence mapping v1')
|
||||
->assertDontSee(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)
|
||||
@ -213,11 +222,13 @@
|
||||
Livewire::actingAs($user)
|
||||
->test(CustomerReviewWorkspace::class)
|
||||
->assertCanSeeTableRecords([$tenant->fresh()])
|
||||
->assertSee('Review recommended')
|
||||
->assertSee('1 evidence signal(s) reference this control.')
|
||||
->assertSee('1 accepted-risk finding(s) qualify this view.')
|
||||
->assertSee('Review the accepted-risk owner and next review date before customer delivery.')
|
||||
->assertSee('Accepted risk influences this view');
|
||||
->assertSee('Review required')
|
||||
->assertSee('Open review')
|
||||
->assertDontSee('Ready for release')
|
||||
->assertDontSee('1 evidence signal(s) reference this control.')
|
||||
->assertDontSee('1 accepted-risk finding(s) qualify this view.')
|
||||
->assertDontSee('Review the accepted-risk owner and next review date before customer delivery.')
|
||||
->assertDontSee('Accepted risk influences this view');
|
||||
});
|
||||
|
||||
it('defaults the customer review workspace to the remembered tenant when tenant context is available', function (): void {
|
||||
|
||||
@ -91,19 +91,27 @@
|
||||
->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('Governance package')
|
||||
->assertSee('Review status')
|
||||
->assertSee('Evidence')
|
||||
->assertSee('Accepted risk status')
|
||||
->assertSee('Last review')
|
||||
->assertSee('Primary action')
|
||||
->assertSee('Download governance package')
|
||||
->assertSee('Anchored to evidence snapshot #')
|
||||
->assertSee('Assessment basis')
|
||||
->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)
|
||||
->assertDontSeeText('Compliance evidence mapping v1')
|
||||
->assertDontSeeText($review->controlInterpretationVersion())
|
||||
->assertDontSeeText('Control readiness interpretation')
|
||||
->assertDontSee('Reason owner')
|
||||
->assertDontSee('Platform reason family')
|
||||
->assertDontSee('Fingerprint')
|
||||
->assertDontSee('Download current review pack')
|
||||
->assertDontSee(OperationRunLinks::tenantlessView((int) $review->operation_run_id), false)
|
||||
->assertDontSee('Inspect the latest review composition or refresh run.');
|
||||
});
|
||||
|
||||
@ -208,6 +208,8 @@ function tenantReviewContractHeaderActions(Testable $component): array
|
||||
->assertActionDoesNotExist('export_executive_pack')
|
||||
->assertActionDoesNotExist('archive_review');
|
||||
|
||||
$component->assertActionExists('download_current_review_pack', fn (Action $action): bool => $action->getLabel() === 'Download governance package');
|
||||
|
||||
$topLevelActionNames = collect(tenantReviewContractHeaderActions($component))
|
||||
->map(static fn ($action): ?string => $action instanceof Action ? $action->getName() : null)
|
||||
->filter()
|
||||
|
||||
@ -2,6 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Finding;
|
||||
use App\Models\User;
|
||||
use App\Services\Findings\FindingExceptionService;
|
||||
use App\Services\TenantReviews\TenantReviewComposer;
|
||||
use App\Support\TenantReviewCompletenessState;
|
||||
use App\Support\TenantReviewStatus;
|
||||
@ -42,3 +45,59 @@
|
||||
->and($payload['status'])->toBe(TenantReviewStatus::Ready->value)
|
||||
->and($payload['summary']['publish_blockers'])->toBe([]);
|
||||
});
|
||||
|
||||
it('derives a governance package summary from existing review and interpretation truth', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$snapshot = seedTenantReviewEvidence($tenant);
|
||||
|
||||
$payload = app(TenantReviewComposer::class)->compose($snapshot);
|
||||
$package = $payload['summary']['governance_package'] ?? null;
|
||||
|
||||
expect($package)->toBeArray()
|
||||
->and($package['delivery_artifact_family'] ?? null)->toBe('review_pack')
|
||||
->and($package['interpretation_version'] ?? null)->toBe('compliance_evidence_mapping.v1')
|
||||
->and($package['executive_summary'] ?? null)->toBeString()
|
||||
->and($package['top_findings'] ?? null)->toBeArray()
|
||||
->and($package['evidence_basis_summary'] ?? null)->toBeString()
|
||||
->and($package['accepted_risks'] ?? null)->toBeArray()
|
||||
->and($package['governance_decisions'] ?? null)->toBeArray()
|
||||
->and($package['supporting_artifact_links'] ?? null)->toBeArray();
|
||||
});
|
||||
|
||||
it('keeps governance decision follow-up entries out of accepted risks', function (): void {
|
||||
[$requester, $tenant] = createUserWithTenant(role: 'owner');
|
||||
$approver = User::factory()->create();
|
||||
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
|
||||
|
||||
/** @var FindingExceptionService $exceptionService */
|
||||
$exceptionService = app(FindingExceptionService::class);
|
||||
|
||||
$validFinding = Finding::factory()->for($tenant)->create([
|
||||
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||
]);
|
||||
|
||||
$requested = $exceptionService->request($validFinding, $tenant, $requester, [
|
||||
'owner_user_id' => (int) $requester->getKey(),
|
||||
'request_reason' => 'Temporary exception while remediation is scheduled',
|
||||
'review_due_at' => now()->addDays(7)->toDateTimeString(),
|
||||
'expires_at' => now()->addDays(14)->toDateTimeString(),
|
||||
]);
|
||||
|
||||
$exceptionService->approve($requested, $approver, [
|
||||
'effective_from' => now()->subDay()->toDateTimeString(),
|
||||
'expires_at' => now()->addDays(14)->toDateTimeString(),
|
||||
'approval_reason' => 'Approved with controls',
|
||||
]);
|
||||
|
||||
$followUpFinding = Finding::factory()->for($tenant)->create([
|
||||
'status' => Finding::STATUS_RISK_ACCEPTED,
|
||||
]);
|
||||
|
||||
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
$package = app(TenantReviewComposer::class)->compose($snapshot)['summary']['governance_package'] ?? [];
|
||||
|
||||
expect(collect($package['accepted_risks'] ?? [])->pluck('finding_id')->all())
|
||||
->toBe([(int) $validFinding->getKey()])
|
||||
->and(collect($package['governance_decisions'] ?? [])->pluck('finding_id')->all())
|
||||
->toBe([(int) $followUpFinding->getKey()]);
|
||||
});
|
||||
|
||||
@ -0,0 +1,51 @@
|
||||
# Specification Quality Checklist: Governance-as-a-Service Packaging v1
|
||||
|
||||
**Purpose**: Validate specification completeness and repo fit before planning
|
||||
**Created**: 2026-05-01
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details leak into the feature contract beyond repo-real route and surface references needed for local convention fit
|
||||
- [x] The spec is focused on user value, trust, sellability, and behavior rather than an implementation diff
|
||||
- [x] The spec is written so non-technical stakeholders can understand the product outcome and scope boundaries
|
||||
- [x] All mandatory sections requested by the repo template and constitution are completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No `[NEEDS CLARIFICATION]` markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic enough to validate product outcome rather than code shape
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions are identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover the primary packaging flows
|
||||
- [x] The feature stays on one bounded on-demand package for one released review context
|
||||
- [x] The spec explicitly depends on the shared customer-safe interpretation layer and forbids bypassing it
|
||||
- [x] The roadmap phrase `open decisions` is narrowed to repo-real exception or risk-acceptance governance decision truth
|
||||
- [x] No new panel, package domain, report engine, schedule, or campaign system is introduced
|
||||
- [x] Existing review, review-pack, evidence, and stored-report surfaces remain the reuse path
|
||||
|
||||
## Test Governance
|
||||
|
||||
- [x] Planned validation stays bounded to focused `confidence` coverage plus one explicit `browser` smoke
|
||||
- [x] The spec reuses existing review, tenant-review, review-pack, and evidence test families instead of introducing a new heavy family
|
||||
- [x] Reviewer handoff and minimal proof commands are explicit
|
||||
|
||||
## Notes
|
||||
|
||||
- Reviewed against `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, `docs/product/implementation-ledger.md`, `specs/257-governance-decision-convergence/spec.md`, `specs/258-customer-review-productization/spec.md`, `specs/259-compliance-evidence-mapping/spec.md`, and `.specify/memory/constitution.md` on 2026-05-01.
|
||||
- No application implementation was performed while preparing this spec.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- **Outcome class**: `repo-fit-ready`
|
||||
- **Outcome**: `keep`
|
||||
- **Reason**: The spec stays narrowly on one management-ready package over existing released-review truth, explicitly records the interpretation dependency, narrows the ambiguous `open decisions` phrase to repo-real governance decision truth, and avoids introducing a second package domain.
|
||||
- **Workflow result**: Ready for `/speckit.plan`
|
||||
@ -0,0 +1,427 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: TenantPilot Governance-as-a-Service Packaging v1 (Conceptual)
|
||||
version: 0.1.0
|
||||
description: |
|
||||
Conceptual contract for the Governance-as-a-Service Packaging v1 planning package.
|
||||
|
||||
These paths describe existing Filament admin and tenant-scoped routes reused by
|
||||
the implementation. The schemas document the derived package summary and
|
||||
package-availability contract expected over existing review, review-pack,
|
||||
evidence, stored-report-backed evidence, and governance-decision truth; they
|
||||
do not define a new public REST API.
|
||||
servers:
|
||||
- url: /
|
||||
paths:
|
||||
/admin/reviews/workspace:
|
||||
get:
|
||||
summary: View management-ready package readiness in the customer review workspace
|
||||
description: |
|
||||
Existing admin-plane workspace page reused as the primary decision surface.
|
||||
The route remains read-only, tenant-safe, and package-readiness remains
|
||||
informational rather than a second competing action path.
|
||||
parameters:
|
||||
- in: query
|
||||
name: tenant
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
description: |
|
||||
Optional tenant prefilter using the current 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 management-ready package context for a released review
|
||||
description: |
|
||||
Existing tenant-scoped released-review detail route reused as the secondary
|
||||
package context surface. The customer-workspace flow uses the existing
|
||||
`customer_workspace=1` query flag to keep the detail read-only and
|
||||
management-ready.
|
||||
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/GovernancePackageDetailModel'
|
||||
'403':
|
||||
description: Forbidden for an in-scope actor missing the inherited review-view capability on the reused route
|
||||
'404':
|
||||
description: Not found for non-members, tenant mismatches, or out-of-scope review targets
|
||||
|
||||
/admin/review-packs/{reviewPack}/download:
|
||||
get:
|
||||
summary: Download the current review-pack-backed governance package artifact
|
||||
description: |
|
||||
Existing signed review-pack download route reused as the default package
|
||||
delivery seam. The management-ready path should reuse this route rather than
|
||||
creating a new package artifact namespace or triggering generation.
|
||||
parameters:
|
||||
- in: path
|
||||
name: reviewPack
|
||||
required: true
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: source_surface
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: review_id
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: tenant_filter_id
|
||||
required: false
|
||||
schema:
|
||||
type: integer
|
||||
- in: query
|
||||
name: interpretation_version
|
||||
required: false
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'302':
|
||||
description: Signed download redirect to the current review-pack artifact
|
||||
'403':
|
||||
description: Forbidden for an in-scope actor missing the review-pack capability or a valid signature
|
||||
'404':
|
||||
description: Not found for non-members, tenant mismatches, or inaccessible review packs
|
||||
|
||||
/admin/t/{tenant}/evidence/{evidenceSnapshot}:
|
||||
get:
|
||||
summary: Open supporting evidence from the management-ready package context
|
||||
description: |
|
||||
Existing tenant-scoped evidence detail route reused only after explicit
|
||||
drilldown from the released-review detail surface and current 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
|
||||
properties:
|
||||
version_key:
|
||||
type: string
|
||||
example: compliance_evidence_mapping.v1
|
||||
display_label:
|
||||
type: string
|
||||
calm_disclosure:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
PackageAvailabilityState:
|
||||
type: object
|
||||
description: Package-facing availability derived from the existing artifact-truth seam. Canonical mapping is publishable -> available, internal_only or stale -> partial, blocked or missing_input -> unavailable, historical_only -> expired, and package-level entitlement restriction -> blocked. Secondary-artifact restrictions do not force the top-level package state to blocked when the summary remains readable.
|
||||
required:
|
||||
- state
|
||||
- message
|
||||
properties:
|
||||
state:
|
||||
type: string
|
||||
enum:
|
||||
- available
|
||||
- partial
|
||||
- unavailable
|
||||
- expired
|
||||
- blocked
|
||||
message:
|
||||
type: string
|
||||
reason_code:
|
||||
type: string
|
||||
nullable: true
|
||||
description: Explains why the package is not fully available without creating a second availability taxonomy.
|
||||
enum:
|
||||
- current_review_pack_missing
|
||||
- current_review_pack_expired
|
||||
- stale_basis
|
||||
- evidence_basis_partial
|
||||
- interpretation_missing
|
||||
- source_not_publishable
|
||||
- entitlement_restricted
|
||||
download_url:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
SupportingArtifactLink:
|
||||
type: object
|
||||
description: Optional proof or artifact drilldown. A supporting link may be forbidden or unavailable while the top-level package summary remains readable.
|
||||
required:
|
||||
- artifact_family
|
||||
- state
|
||||
- label
|
||||
properties:
|
||||
artifact_family:
|
||||
type: string
|
||||
enum:
|
||||
- review_pack
|
||||
- evidence_snapshot
|
||||
- stored_report
|
||||
state:
|
||||
type: string
|
||||
enum:
|
||||
- available
|
||||
- partial
|
||||
- unavailable
|
||||
- expired
|
||||
- redacted
|
||||
- forbidden
|
||||
label:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
nullable: true
|
||||
url:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
ManagementFindingHighlight:
|
||||
type: object
|
||||
required:
|
||||
- label
|
||||
- customer_summary
|
||||
properties:
|
||||
finding_id:
|
||||
type: integer
|
||||
nullable: true
|
||||
label:
|
||||
type: string
|
||||
severity:
|
||||
type: string
|
||||
nullable: true
|
||||
customer_summary:
|
||||
type: string
|
||||
control_key:
|
||||
type: string
|
||||
nullable: true
|
||||
|
||||
GovernanceDecisionFollowUp:
|
||||
type: object
|
||||
required:
|
||||
- decision_source
|
||||
- decision_state
|
||||
- decision_summary
|
||||
properties:
|
||||
decision_source:
|
||||
type: string
|
||||
enum:
|
||||
- finding_exception
|
||||
- risk_acceptance
|
||||
decision_state:
|
||||
type: string
|
||||
enum:
|
||||
- awareness_required
|
||||
- expiring
|
||||
- expired
|
||||
- revoked
|
||||
- missing_basis
|
||||
decision_summary:
|
||||
type: string
|
||||
accountable_label:
|
||||
type: string
|
||||
nullable: true
|
||||
review_due_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
expires_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
|
||||
GovernancePackageSummary:
|
||||
type: object
|
||||
required:
|
||||
- review_id
|
||||
- tenant_id
|
||||
- interpretation
|
||||
- delivery_artifact_family
|
||||
- package_availability
|
||||
- executive_summary
|
||||
properties:
|
||||
review_id:
|
||||
type: integer
|
||||
tenant_id:
|
||||
type: integer
|
||||
interpretation:
|
||||
$ref: '#/components/schemas/ControlInterpretationVersion'
|
||||
delivery_artifact_family:
|
||||
type: string
|
||||
enum:
|
||||
- review_pack
|
||||
package_availability:
|
||||
$ref: '#/components/schemas/PackageAvailabilityState'
|
||||
executive_summary:
|
||||
type: string
|
||||
highlights:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
top_findings:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/ManagementFindingHighlight'
|
||||
accepted_risks:
|
||||
type: array
|
||||
description: Currently valid tolerated-risk positions that explain why material risk is presently accepted and does not require immediate stakeholder follow-up beyond awareness.
|
||||
items:
|
||||
$ref: '#/components/schemas/GovernanceDecisionFollowUp'
|
||||
governance_decisions:
|
||||
type: array
|
||||
description: Accepted-risk or exception decision entries that still require stakeholder awareness or follow-up. Entries must be disjoint from accepted_risks for the same review.
|
||||
items:
|
||||
$ref: '#/components/schemas/GovernanceDecisionFollowUp'
|
||||
evidence_basis_summary:
|
||||
type: string
|
||||
nullable: true
|
||||
recommended_next_action:
|
||||
type: string
|
||||
nullable: true
|
||||
supporting_artifacts:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SupportingArtifactLink'
|
||||
|
||||
CustomerReviewWorkspaceEntry:
|
||||
type: object
|
||||
required:
|
||||
- tenant_id
|
||||
- tenant_name
|
||||
- latest_published_review_id
|
||||
- package_availability
|
||||
- control_readiness
|
||||
properties:
|
||||
tenant_id:
|
||||
type: integer
|
||||
tenant_name:
|
||||
type: string
|
||||
latest_published_review_id:
|
||||
type: integer
|
||||
latest_review_published_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
interpretation:
|
||||
$ref: '#/components/schemas/ControlInterpretationVersion'
|
||||
package_availability:
|
||||
$ref: '#/components/schemas/PackageAvailabilityState'
|
||||
control_readiness:
|
||||
type: string
|
||||
evidence_basis:
|
||||
type: string
|
||||
nullable: true
|
||||
recommended_next_action:
|
||||
type: string
|
||||
nullable: true
|
||||
evidence_proof_state:
|
||||
type: string
|
||||
nullable: true
|
||||
management_teaser:
|
||||
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'
|
||||
|
||||
GovernancePackageDetailModel:
|
||||
type: object
|
||||
required:
|
||||
- review
|
||||
- package
|
||||
properties:
|
||||
review:
|
||||
type: object
|
||||
required:
|
||||
- review_id
|
||||
- tenant_id
|
||||
- published_at
|
||||
properties:
|
||||
review_id:
|
||||
type: integer
|
||||
tenant_id:
|
||||
type: integer
|
||||
published_at:
|
||||
type: string
|
||||
format: date-time
|
||||
nullable: true
|
||||
package:
|
||||
$ref: '#/components/schemas/GovernancePackageSummary'
|
||||
317
specs/260-governance-service-packaging/data-model.md
Normal file
317
specs/260-governance-service-packaging/data-model.md
Normal file
@ -0,0 +1,317 @@
|
||||
# Data Model — Governance-as-a-Service Packaging v1
|
||||
|
||||
**Spec**: [spec.md](spec.md)
|
||||
|
||||
No new persisted table, artifact family, or package projection store is required for this feature. The package remains a derived management-ready view over current review, section, review-pack, evidence, stored-report, governance-decision, entitlement, and audit truth.
|
||||
|
||||
## Source Truth Reused
|
||||
|
||||
### Workspace / Tenant Entitlement Context
|
||||
|
||||
**Purpose**: Establish the active workspace boundary and entitled tenant set before workspace rows, released-review detail, package download, or supporting artifact links resolve.
|
||||
|
||||
**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 current workspace session model
|
||||
|
||||
**Validation rules**:
|
||||
- actors outside the current workspace or tenant scope resolve as not found
|
||||
- package readiness is only aggregated for entitled tenants with eligible released reviews
|
||||
- supporting artifact availability must not leak inaccessible tenant or review presence
|
||||
|
||||
### ReleasedReviewContext
|
||||
|
||||
**Purpose**: Existing tenant review that anchors all package content and package-access semantics.
|
||||
|
||||
**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`
|
||||
- `tenant`
|
||||
- `evidenceSnapshot`
|
||||
- `sections`
|
||||
|
||||
**Validation / usage rules**:
|
||||
- only released reviews feed the management-ready package path
|
||||
- the review remains the canonical package-owning context; package output must not become independent truth about review state
|
||||
- existing `summary.control_interpretation`, `summary.highlights`, and related summary data remain the first reuse path for package meaning
|
||||
|
||||
### TenantReviewSection Family
|
||||
|
||||
**Purpose**: Existing released-review section set that already captures the management-relevant content needed for packaging.
|
||||
|
||||
**Persisted carrier**: existing `tenant_review_sections` rows via [../../apps/platform/app/Models/TenantReviewSection.php](../../apps/platform/app/Models/TenantReviewSection.php)
|
||||
|
||||
**Current section keys already relevant to packaging**:
|
||||
- `executive_summary`
|
||||
- `control_interpretation`
|
||||
- `open_risks`
|
||||
- `accepted_risks`
|
||||
- `permission_posture`
|
||||
- `baseline_drift_posture`
|
||||
- `operations_health`
|
||||
|
||||
**Relevant fields**:
|
||||
- `section_key`
|
||||
- `title`
|
||||
- `sort_order`
|
||||
- `completeness_state`
|
||||
- `summary_payload`
|
||||
- `render_payload`
|
||||
- `measured_at`
|
||||
|
||||
**Validation / usage rules**:
|
||||
- package sections should be rendered from this current section family before any new payload is considered
|
||||
- accepted-risk and governance-decision follow-up stays anchored to the current `accepted_risks` section and related persisted decision truth
|
||||
- if implementation later needs a small additional helper field, it must stay embedded inside existing review payloads rather than creating a new package entity
|
||||
|
||||
### Shared Interpretation Summary
|
||||
|
||||
**Purpose**: Existing management-meaning layer from Spec 259 that translates technical review truth into calm customer-safe language.
|
||||
|
||||
**Primary producer**: [../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php](../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php) via [../../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)
|
||||
|
||||
**Current carriers**:
|
||||
- `TenantReview.summary['control_interpretation']`
|
||||
- `TenantReviewSection` with `section_key = control_interpretation`
|
||||
|
||||
**Relevant fields / payload**:
|
||||
- interpretation version
|
||||
- control readiness summaries
|
||||
- evidence-basis summaries
|
||||
- limitation and follow-up wording
|
||||
- per-control entries rendered for deeper detail
|
||||
|
||||
**Validation / usage rules**:
|
||||
- package meaning must remain dependent on this shared interpretation layer
|
||||
- if the interpretation layer or version is unavailable, the package must show explicit partial or unavailable state and must not infer management meaning directly from raw findings or stored reports
|
||||
|
||||
### ReviewPack
|
||||
|
||||
**Purpose**: Existing packaged export artifact reused as the default package-delivery seam for stakeholder download.
|
||||
|
||||
**Persisted carrier**: existing `review_packs` rows via [../../apps/platform/app/Models/ReviewPack.php](../../apps/platform/app/Models/ReviewPack.php)
|
||||
|
||||
**Relevant fields / relationships**:
|
||||
- `id`
|
||||
- `workspace_id`
|
||||
- `tenant_id`
|
||||
- `tenant_review_id`
|
||||
- `status`
|
||||
- `expires_at`
|
||||
- `summary`
|
||||
- `options`
|
||||
- `tenantReview`
|
||||
- `evidenceSnapshot`
|
||||
|
||||
**Validation / usage rules**:
|
||||
- the package path should prefer reusing `currentExportReviewPack` and its signed download route
|
||||
- management-ready package access must not create a new review-pack generation run or a new package artifact family
|
||||
- review-pack readiness, expiry, missing-input, and blocked states remain review-pack truth, not new package lifecycle state
|
||||
|
||||
### 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**:
|
||||
- evidence remains supporting truth and must stay distinct from package summary truth
|
||||
- default-visible package content may summarize the evidence basis, but raw evidence payloads remain secondary and capability-gated
|
||||
- supporting proof access should reuse existing evidence routes and audit events
|
||||
|
||||
### StoredReport-backed Evidence Inputs
|
||||
|
||||
**Purpose**: Existing report artifacts that feed evidence dimensions and therefore indirectly support the package evidence basis.
|
||||
|
||||
**Persisted carriers**:
|
||||
- existing `stored_reports` rows via [../../apps/platform/app/Models/StoredReport.php](../../apps/platform/app/Models/StoredReport.php)
|
||||
- existing evidence-source seams such as [../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php](../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php) and [../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php](../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php)
|
||||
|
||||
**Relevant fields / contracts**:
|
||||
- `StoredReport.id`
|
||||
- `workspace_id`
|
||||
- `tenant_id`
|
||||
- `report_type`
|
||||
- `fingerprint`
|
||||
- current evidence-source `source_kind = stored_report`
|
||||
- current evidence-source `source_record_id`
|
||||
|
||||
**Validation / usage rules**:
|
||||
- stored reports remain subordinate source artifacts behind evidence and review truth
|
||||
- the package may mention stored-report-backed evidence basis, but it must not default to raw report payloads or invent a new viewer shell in v1
|
||||
- if no current entitled viewer seam exists, the package should prefer explicit unavailable or secondary context messaging over a new route
|
||||
|
||||
### GovernanceDecisionTruth
|
||||
|
||||
**Purpose**: Existing accepted-risk and exception decision truth that qualifies management-ready package claims and powers the narrowed `open decisions` meaning.
|
||||
|
||||
**Persisted carriers**:
|
||||
- existing `finding_exceptions` rows via [../../apps/platform/app/Models/FindingException.php](../../apps/platform/app/Models/FindingException.php)
|
||||
- existing `finding_exception_decisions` truth referenced by the current exception relationships
|
||||
|
||||
**Relevant fields / relationships**:
|
||||
- exception `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**:
|
||||
- `open decisions` remains narrowed to this existing accepted-risk / exception decision truth only
|
||||
- package wording may summarize accountable role or person, decision reason, follow-up, and validity timing where current product truth exists
|
||||
- missing owner, approval, or timing truth must surface as explicit partial disclosure instead of invented certainty
|
||||
|
||||
### Audit Log Event Family
|
||||
|
||||
**Purpose**: Existing audit trail used to keep package access and supporting artifact consumption attributable.
|
||||
|
||||
**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`
|
||||
- `review_pack.downloaded`
|
||||
- `evidence_snapshot.opened`
|
||||
|
||||
**Validation / usage rules**:
|
||||
- no new audit store or audit family is justified by default
|
||||
- package access and supporting artifact access should reuse these events with shared metadata before introducing a new event concept
|
||||
|
||||
## Derived Contracts
|
||||
|
||||
### GovernancePackageSummaryProjection
|
||||
|
||||
**Purpose**: Conceptual derived package contract rendered from the released review and supporting artifacts.
|
||||
|
||||
**Persistence**: not persisted independently; derived from `TenantReview.summary`, existing section payloads, current review-pack truth, evidence truth, and governance-decision truth
|
||||
|
||||
**Fields**:
|
||||
- `review_id`
|
||||
- `tenant_id`
|
||||
- `interpretation_version`
|
||||
- `delivery_artifact_family = review_pack`
|
||||
- `package_availability`
|
||||
- `executive_summary`
|
||||
- `top_findings[]`
|
||||
- `accepted_risks[]`
|
||||
- `governance_decisions[]`
|
||||
- `evidence_basis_summary`
|
||||
- `supporting_artifact_links[]`
|
||||
- `calm_disclosure`
|
||||
|
||||
**Field boundary rules**:
|
||||
- `accepted_risks[]` contains currently valid tolerated-risk positions that explain why a material issue is presently accepted and does not need immediate stakeholder follow-up beyond awareness.
|
||||
- `governance_decisions[]` contains accepted-risk or exception decision entries that still require stakeholder awareness because they are expiring, expired, revoked, missing basis, or otherwise need follow-up.
|
||||
- the same underlying accepted-risk or exception decision must not appear in both arrays for the same released review; if an entry qualifies for both, prefer `governance_decisions[]`.
|
||||
|
||||
**Validation rules**:
|
||||
- the projection must not become independent product truth about review, evidence, pack, or governance state
|
||||
- the downloadable artifact for v1 remains the current export review pack for the released review; the governance package summary is derived framing over that existing artifact, not a second export family
|
||||
- package meaning must stay consistent between workspace summary, released-review detail, and package delivery for the same released review
|
||||
- if implementation later needs a tiny helper shape, it must stay embedded inside current review/resource helpers rather than new persistence
|
||||
|
||||
### PackageAvailabilityState
|
||||
|
||||
**Purpose**: Derived explanation of whether the current released review can deliver the management-ready package now.
|
||||
|
||||
**Persistence**: derived from current review-pack truth, evidence basis completeness, interpretation availability, and entitlement state
|
||||
|
||||
**Planned values**:
|
||||
- `available`
|
||||
- `partial`
|
||||
- `unavailable`
|
||||
- `expired`
|
||||
- `blocked`
|
||||
|
||||
**Validation rules**:
|
||||
- these are presentation semantics only and must not become a new persisted lifecycle family
|
||||
- stale or incomplete basis resolves through `partial` or `unavailable` reasons such as `stale_basis` or `evidence_basis_partial`, not a separate top-level state
|
||||
- entitlement-restricted package access resolves through `blocked` or per-artifact `forbidden` semantics, not a separate top-level availability state
|
||||
- reasons should remain attributable to the underlying review-pack, evidence, entitlement, or interpretation truth
|
||||
|
||||
**Canonical mapping from current artifact-truth seam**:
|
||||
|
||||
| Current repo truth | Package state | Reason-code guidance | Notes |
|
||||
|---|---|---|---|
|
||||
| `publicationReadiness = publishable` and current pack truth | `available` | `null` unless a secondary artifact has its own issue | The current export review pack is ready for stakeholder delivery. |
|
||||
| `publicationReadiness = internal_only` or freshness `stale` while the summary is still readable | `partial` | `stale_basis` or `evidence_basis_partial` | Keep the management summary readable, but be explicit that the delivery basis is incomplete or stale. |
|
||||
| `publicationReadiness = blocked`, `artifactExistence = not_created`, `contentState = missing_input`, or interpretation/source truth is not publishable | `unavailable` | `current_review_pack_missing`, `interpretation_missing`, or `source_not_publishable` | The package cannot be truthfully delivered now, so do not imply download readiness. |
|
||||
| `artifactExistence = historical_only` or current pack expiry is the governing truth | `expired` | `current_review_pack_expired` | Historical exports stay attributable, but they are not the current stakeholder package. |
|
||||
| In-scope actor lacks package entitlement to the released-review package path | `blocked` | `entitlement_restricted` | The package path itself is gated and must not render as deliverable. |
|
||||
| Package summary is readable, but one supporting proof or artifact link is restricted | package state stays `available` or `partial` according to review-pack truth; affected secondary link becomes `forbidden` or `unavailable` | `supporting_access_limited` belongs on the supporting link message, not on the top-level package state | Keep package-level access and per-artifact access distinct. |
|
||||
|
||||
**Additional mapping notes**:
|
||||
- repo directions such as `usable` stay inside `SupportingArtifactLink.state` and do not create a sixth package state
|
||||
- repo directions such as `follow_up_needed` affect governance-follow-up wording and calm disclosure, not top-level package availability on their own
|
||||
|
||||
### GovernanceDecisionFollowUp
|
||||
|
||||
**Purpose**: Derived management-readable summary of accepted-risk / exception decisions that still need stakeholder awareness.
|
||||
|
||||
**Persistence**: derived from current exception and decision truth only
|
||||
|
||||
**Fields**:
|
||||
- `decision_source`
|
||||
- `decision_state`
|
||||
- `decision_summary`
|
||||
- `accountable_label` (nullable)
|
||||
- `review_due_at` (nullable)
|
||||
- `expires_at` (nullable)
|
||||
- `follow_up_message`
|
||||
|
||||
**Validation rules**:
|
||||
- follow-up entries must not imply a broader queue or workflow board
|
||||
- only currently valid accepted-risk / exception decision truth may feed this projection
|
||||
|
||||
### SupportingArtifactLink
|
||||
|
||||
**Purpose**: Derived representation of optional proof and artifact drilldowns from the package.
|
||||
|
||||
**Persistence**: derived from current evidence, review-pack, and existing viewer availability
|
||||
|
||||
**Fields**:
|
||||
- `artifact_family`
|
||||
- `state`
|
||||
- `label`
|
||||
- `message`
|
||||
- `url` (nullable)
|
||||
|
||||
**Validation rules**:
|
||||
- links must point only to existing entitled surfaces
|
||||
- if no current viewer seam exists for a stored-report-backed detail, the link stays unavailable rather than introducing a new route
|
||||
- the package must not duplicate raw payloads when a secondary artifact link is unavailable
|
||||
322
specs/260-governance-service-packaging/plan.md
Normal file
322
specs/260-governance-service-packaging/plan.md
Normal file
@ -0,0 +1,322 @@
|
||||
# Implementation Plan: Governance-as-a-Service Packaging v1
|
||||
|
||||
**Branch**: `260-governance-service-packaging` | **Date**: 2026-05-01 | **Spec**: [spec.md](spec.md)
|
||||
**Input**: Feature specification from [spec.md](spec.md)
|
||||
|
||||
## Summary
|
||||
|
||||
Prepare one bounded, on-demand management-ready governance package inside the existing customer review workspace and released-review detail flow. The narrow implementation path is to reuse current released-review summary and section truth, the shared `control_interpretation` layer from Spec 259, the existing current-review-pack signed-download seam, and the shared artifact-truth presenter for availability or expiry semantics, while keeping package access read-only, tenant-safe, auditable, localization-ready, and derived.
|
||||
|
||||
Repo truth already supports the key seams this feature needs: [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) already exposes `control_readiness`, `evidence_basis`, `recommended_next_action`, and `evidence_proof_state` and audits workspace opens; [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) already has a customer-workspace-specific read-only mode with `downloadCurrentReviewPackAction()` while keeping `exportExecutivePackAction()` operator-only; [../../apps/platform/app/Services/ReviewPackService.php](../../apps/platform/app/Services/ReviewPackService.php) already provides review-derived pack generation and signed download URLs; and [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) already understands `TenantReview`, `EvidenceSnapshot`, and `ReviewPack` truth.
|
||||
|
||||
V1 therefore stays narrow: no new panel or provider, no new package artifact family, no new report engine, no schedule or batch flow, no campaign system, no AI summary layer, no PSA or CRM workflow, no package-generation run from the management-ready path, and no cross-tenant packaging.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4, Laravel 12
|
||||
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing `TenantReviewComposer`, `TenantReviewSectionFactory`, `ComplianceEvidenceMappingV1`, `ReviewPackService`, `ArtifactTruthPresenter`, capability helpers, localization copy, and shared audit infrastructure
|
||||
**Storage**: PostgreSQL via existing `tenant_reviews`, `tenant_review_sections`, `review_packs`, `evidence_snapshots`, `evidence_snapshot_items`, `stored_reports`, `findings`, `finding_exceptions`, `finding_exception_decisions`, memberships, and `audit_logs`; no new persistence planned
|
||||
**Testing**: Pest v4 feature and focused unit coverage plus one bounded browser smoke on the existing review flow
|
||||
**Validation Lanes**: confidence, browser
|
||||
**Target Platform**: Laravel monolith in `apps/platform`, existing admin plane only (`/admin` plus existing tenant-scoped `/admin/t/{tenant}` reuse)
|
||||
**Project Type**: Web application (Laravel monolith with Filament pages/resources)
|
||||
**Performance Goals**: keep workspace and released-review rendering DB-only and scope-safe, reuse already composed review summary and section payloads plus current review-pack or evidence truth, and avoid new Graph calls, queue starts, or heavy asset work on the read path
|
||||
**Constraints**: no new panel/provider, no new `GovernancePackage` persistence family, no new report engine, no scheduling/batching/campaign system, no AI summaries, no PSA/CRM workflow, no package-generation runs from the management-ready path, no cross-tenant packaging, no global-search expansion, and no asset strategy change
|
||||
**Scale/Scope**: 1 existing workspace page, 1 existing released-review detail page, 1 existing signed review-pack download route, existing evidence and review-pack truth seams, existing stored-report-backed evidence sources, shared audit infrastructure, and focused reuse of the current `Reviews`, `TenantReview`, `ReviewPack`, `Evidence`, and browser test families
|
||||
|
||||
## Likely Affected Repo Surfaces
|
||||
|
||||
- [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) for package-readiness wording, calm disclosure, and consistency with the current `control_readiness`, `evidence_basis`, `recommended_next_action`, and `evidence_proof_state` columns.
|
||||
- [../../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 package framing and management-ready disclosure at the page-intro level.
|
||||
- [../../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 released-review detail contract, customer-workspace read-only mode, one dominant package action, and operator-only export separation.
|
||||
- [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php) and [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php) for the already composed `summary.control_interpretation`, `summary.highlights`, and current section family (`executive_summary`, `control_interpretation`, `open_risks`, `accepted_risks`, `permission_posture`, `baseline_drift_posture`, `operations_health`).
|
||||
- [../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php](../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php) for the shared interpretation dependency and existing accepted-risk follow-up language.
|
||||
- [../../apps/platform/app/Services/ReviewPackService.php](../../apps/platform/app/Services/ReviewPackService.php) and [../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php](../../apps/platform/app/Http/Controllers/ReviewPackDownloadController.php) for current review-pack truth, signed downloads, and current audit boundaries.
|
||||
- [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) and related envelope/compression helpers for `TenantReview`, `EvidenceSnapshot`, and `ReviewPack` availability, freshness, and artifact/result truth separation.
|
||||
- [../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php](../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php) and [../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php](../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php) as the current stored-report-backed evidence seams; planning should avoid inventing a new stored-report viewer when these sources already feed the review and evidence basis.
|
||||
- [../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php](../../apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php) and existing evidence detail pages for secondary proof routes and explicit unavailable-state behavior.
|
||||
- [../../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 reusable audit events and metadata.
|
||||
- [../../apps/platform/app/Services/Auth/RoleCapabilityMap.php](../../apps/platform/app/Services/Auth/RoleCapabilityMap.php) and [../../apps/platform/app/Support/Auth/Capabilities.php](../../apps/platform/app/Support/Auth/Capabilities.php) for capability-first RBAC and deny-as-not-found boundaries.
|
||||
- [../../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 management-ready and customer-safe wording.
|
||||
- [../../apps/platform/tests/Unit/TenantReview/TenantReviewComposerTest.php](../../apps/platform/tests/Unit/TenantReview/TenantReviewComposerTest.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/Reviews/CustomerReviewWorkspacePackAccessTest.php](../../apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewAuditLogTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewAuditLogTest.php), [../../apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php](../../apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php), [../../apps/platform/tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php](../../apps/platform/tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php), [../../apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php](../../apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php), [../../apps/platform/tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php](../../apps/platform/tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php), [../../apps/platform/tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php](../../apps/platform/tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php), [../../apps/platform/tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.php](../../apps/platform/tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.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 proving lane already present in the repo.
|
||||
|
||||
## Packaging / Artifact Reuse Fit
|
||||
|
||||
- Keep the released review as the only package-owning context. The package summary should be derived from the already composed review summary and section payloads rather than from a new `GovernancePackage` record or a new report namespace.
|
||||
- Repo truth already prefers current review-pack reuse over package generation on the customer-safe path. In customer-workspace mode [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) exposes `downloadCurrentReviewPackAction()` only and routes it through the current signed-download URL, while `exportExecutivePackAction()` remains outside the customer-workspace flow.
|
||||
- The management-ready path should therefore begin with current `currentExportReviewPack` availability, signed download reuse, and explicit unavailable or expired states. It must not call [../../apps/platform/app/Services/ReviewPackService.php](../../apps/platform/app/Services/ReviewPackService.php)`::generateFromReview()` from the package path and must not start a new `OperationRun` or report-generation sequence.
|
||||
- In v1, `Download governance package` is governance-package framing for downloading the current export review pack for the released review. The summary shown on the page is derived presentation over that artifact and related evidence truth, not a second downloadable export family.
|
||||
- [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) already has `forReviewPack()`, `forEvidenceSnapshot()`, and tenant-review seams. Package readiness, expiration, blocked, historical-only, or missing-input states should piggyback on those existing artifact-truth semantics instead of creating a new package state family.
|
||||
- Stored reports remain subordinate source truth. The repo currently exposes them mainly through evidence-source and widget seams, not through an obvious shared stored-report viewer resource. V1 should therefore summarize stored-report-backed evidence basis where needed and use explicit unavailable or secondary existing-viewer behavior instead of inventing a new stored-report viewer route.
|
||||
- The roadmap phrase `open decisions` must stay narrowed to current accepted-risk and exception decision truth from `FindingException` and `FindingExceptionDecision`. The package does not become a broader governance inbox or approval queue.
|
||||
|
||||
## 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. This plan tightens the existing page instead of adding a new page class, Resource, panel, provider, or cluster.
|
||||
- Keep [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) as the only secondary context surface, using the existing customer-workspace read-only mode rather than creating a new package detail shell.
|
||||
- Filament remains v5 on Livewire v4. Provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), and this feature does not plan any panel or provider changes.
|
||||
- Global search posture stays unchanged. [../../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) are already globally disabled, and this plan adds no new globally searchable resource.
|
||||
- Keep one dominant next action per surface. On the workspace page that remains `Open released review`; package readiness stays informational and should not become a competing row action. On released-review detail the dominant action remains a safe package download path rooted in the current review-pack seam.
|
||||
- No destructive actions are introduced for the management-ready package path. Existing destructive or mutating review lifecycle actions on the normal operator detail page remain outside this slice and continue to rely on existing authorization and confirmation behavior.
|
||||
- Asset strategy remains unchanged. No new Filament asset registration is planned. If a later implementation unexpectedly registers assets, deployment still uses the existing `cd apps/platform && php artisan filament:assets` step, but no such change is expected here.
|
||||
|
||||
## RBAC / Policy Fit
|
||||
|
||||
- Workspace membership remains the first isolation boundary via the current workspace context and [../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php](../../apps/platform/app/Services/TenantReviews/TenantReviewRegisterService.php). Non-members and actors without any entitled tenant in the current workspace remain `404`.
|
||||
- Tenant scope remains the second isolation boundary. Released-review detail, review-pack download, evidence proof, and any secondary drilldown must stay tied to the selected tenant and deny cross-tenant access as not found.
|
||||
- Capability reuse remains mandatory. Package access should ride current review visibility plus current `REVIEW_PACK_VIEW` and `EVIDENCE_VIEW` capability checks rather than introducing raw strings or role-name branches.
|
||||
- Inside an established scope, the reused released-review detail route or secondary artifact access may still fail as capability denial when inherited capability checks deny access. The page-level contract should prefer calm unavailable messaging where the summary remains readable, while inherited detail-route and direct execution endpoints keep current `403` or signed-route enforcement semantics.
|
||||
- Cross-tenant packaging is explicitly out of scope. The workspace may only show package readiness for entitled tenants and their eligible released reviews.
|
||||
|
||||
## Audit / Logging Fit
|
||||
|
||||
- Reuse [../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php](../../apps/platform/app/Services/Audit/WorkspaceAuditLogger.php) and [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) as the only audit path.
|
||||
- [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) already logs `CustomerReviewWorkspaceOpened` with `source_surface`, tenant-filter metadata, and interpretation-version context.
|
||||
- [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) already logs `TenantReviewOpened` with the customer-workspace source and interpretation version when entered from the workspace.
|
||||
- Current review-pack downloads already flow through `ReviewPackDownloaded` on the signed route. Planning should prefer reusing that event plus metadata over creating a new package-download audit family.
|
||||
- Secondary proof access should remain on existing evidence-open audit paths. If a distinct management-ready package open event is ever proposed beyond current review open and pack download, that should first be justified as additive metadata on the existing audit path before a new event family is considered.
|
||||
|
||||
## Data & Query Fit
|
||||
|
||||
- Package summary should start from existing released-review truth already composed in [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php): `summary.control_interpretation`, `summary.highlights`, `summary.recommended_next_actions`, and the current evidence-basis block.
|
||||
- Detailed package sections should reuse the current section family from [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php): `executive_summary`, `control_interpretation`, `open_risks`, `accepted_risks`, `permission_posture`, `baseline_drift_posture`, and `operations_health`.
|
||||
- Governance-decision follow-up must stay rooted in accepted-risk and exception truth. That means the package should summarize `accepted_risks` section output and underlying `FindingException` or `FindingExceptionDecision` validity, owner, approval, expiry, and follow-up semantics without broadening into a new decision framework.
|
||||
- Review-pack availability should be derived from current review-pack truth, signed-download readiness, expiry, and artifact-truth state. It should not become a new persisted package lifecycle.
|
||||
- Evidence and stored-report basis should remain distinct from the package summary. Stored reports can remain subordinate evidence carriers behind existing evidence-source seams; the package may summarize their role, but it should not surface raw report payloads or invent a new viewer if one is not already present.
|
||||
- The default plan is render-time composition over released-review summary, sections, and current artifact truth. If implementation later proves that one tiny helper accessor is needed, it should stay inside existing review/resource helpers and must not become new persistence or a parallel package service framework.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed surfaces
|
||||
- **Native vs custom classification summary**: native Filament
|
||||
- **Shared-family relevance**: status messaging, management summary sections, action links, evidence and review-pack viewers, package-availability states, and audit labels
|
||||
- **State layers in scope**: page, detail, URL-query, table/session restore
|
||||
- **Audience modes in scope**: customer/read-only, customer-admin, auditor-read-only, operator-MSP
|
||||
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
|
||||
- **Raw/support gating plan**: collapsed and capability-gated on reused evidence or review-pack paths; stored-report detail remains secondary and only where an existing viewer seam already exists
|
||||
- **One-primary-action / duplicate-truth control**: the workspace keeps `Open released review` as the sole dominant row action; released-review detail keeps one dominant package action; the workspace states readiness while detail owns the package summary so equal-priority duplicate truth is avoided
|
||||
- **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 new package artifact, new report viewer, new branding or profile engine, or package-generation workflow becomes exception-required drift
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes
|
||||
- **Systems touched**: `CustomerReviewWorkspace`, `TenantReviewResource`, `ViewTenantReview`, `TenantReviewComposer`, `TenantReviewSectionFactory`, `ComplianceEvidenceMappingV1`, `ReviewPackService`, `ReviewPackDownloadController`, `ArtifactTruthPresenter`, existing evidence-source seams, localization copy, `WorkspaceAuditLogger`, and `AuditActionId`
|
||||
- **Shared abstractions reused**: existing review summary and section payloads, the shared `control_interpretation` contract, `ArtifactTruthPresenter` plus envelope/compression seams, current signed review-pack download generation, capability helpers, and the shared audit logger
|
||||
- **New abstraction introduced? why?**: none planned. If implementation needs a tiny package-view accessor, it should stay inside the existing tenant-review or review-pack family instead of becoming a new packaging framework
|
||||
- **Why the existing abstraction was sufficient or insufficient**: existing review, interpretation, review-pack, evidence, and artifact-truth seams already describe what the package should say and whether its supporting artifacts are usable. What is missing is the bounded product contract over those seams, not a new domain model or engine
|
||||
- **Bounded deviation / spread control**: none planned. If a real stored-report viewer gap or profile-variant need appears, record it as a follow-up instead of adding a local workaround that becomes permanent architecture
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no
|
||||
- **Central contract reused**: `N/A`
|
||||
- **Delegated UX behaviors**: `N/A`
|
||||
- **Surface-owned behavior kept local**: read-only workspace and released-review detail rendering only; the management-ready path must not enqueue review-pack, stored-report, or evidence generation
|
||||
- **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**: stored-report types, evidence dimension keys, and Microsoft-shaped source detail remain provider-owned inputs inside the existing evidence-source and report layers
|
||||
- **Platform-core seams**: `governance package`, `released review`, `accepted risk`, `governance decision`, `evidence basis`, management-ready summary wording, and package-availability meaning
|
||||
- **Neutral platform terms / contracts preserved**: `governance package`, `released review`, `review pack`, `evidence basis`, `accepted risk`, `governance decision`, and `review context`
|
||||
- **Retained provider-specific semantics and why**: provider-specific labels may remain visible only in entitled secondary detail where an existing viewer already exposes them. They must not become default package vocabulary
|
||||
- **Bounded extraction or follow-up path**: `document-in-feature` for bounded wording and truth-separation notes; `follow-up-spec` only if later work demands framework-specific package profiles or a real stored-report viewer surface
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first / snapshot truth: PASS. The slice consumes existing released reviews, evidence snapshots, stored-report-backed evidence, and review-pack artifacts as read-only truth.
|
||||
- Read/write separation: PASS. No new mutation, publication, regeneration, schedule, batch, or destructive path is introduced.
|
||||
- Graph contract path: PASS. No new Graph call or provider contract work is part of this feature.
|
||||
- Deterministic capabilities: PASS. Existing capability registries and role maps remain canonical.
|
||||
- Workspace and tenant isolation: PASS. Workspace membership and tenant entitlement remain `404` boundaries.
|
||||
- RBAC-UX plane separation: PASS. Everything stays inside the existing `/admin` plane and current tenant-scoped detail/proof reuse.
|
||||
- Destructive confirmation standard: PASS by non-use. The package path introduces no destructive action, and existing operator-only destructive lifecycle actions remain unchanged outside this slice.
|
||||
- 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 management-ready path must not call `generateFromReview()` or start any report/evidence generation workflow.
|
||||
- Data minimization: PASS. Default-visible content stays management-ready and customer-safe; raw reports, provider detail, and support diagnostics remain secondary.
|
||||
- Test governance (TEST-GOV-001): PASS. Planned proof stays inside focused unit/feature coverage plus one bounded browser smoke.
|
||||
- Proportionality / no premature abstraction: PASS. The package stays a derived contract over existing review, interpretation, pack, and artifact truth instead of introducing new persistence, engines, or registries.
|
||||
- Persisted truth (PERSIST-001): PASS. No new table, entity, or artifact family is planned.
|
||||
- Behavioral state (STATE-001): PASS. `available`, `partial`, `unavailable`, `expired`, and `blocked` conditions remain derived from existing review-pack and artifact truth, while stale or entitlement-restricted situations remain encoded as reasons within those states rather than as a new package lifecycle.
|
||||
- Canonical package-state mapping: current repo truth `publishable` maps to `available`; `internal_only` or `stale` with still-readable basis maps to `partial`; `blocked`, `missing_input`, or source-not-publishable cases map to `unavailable`; `historical_only` maps to `expired`; package-level entitlement restriction maps to `blocked`. Secondary proof restrictions stay on `SupportingArtifactLink` disclosure and do not force the package summary itself into `blocked` when the review remains readable. Repo directions like `usable` or `follow_up_needed` stay inside supporting-artifact or governance-follow-up disclosure rather than creating extra package states.
|
||||
- UI semantics / shared pattern first / Filament-native UI: PASS. Native Filament surfaces and existing shared review/artifact seams remain the default path.
|
||||
- Provider boundary (PROV-001): PASS. Provider-specific semantics stay inside current source artifacts and do not become platform-core package truth.
|
||||
- Filament / Laravel planning contract: PASS. Filament remains v5 on Livewire v4, provider registration remains in [../../apps/platform/bootstrap/providers.php](../../apps/platform/bootstrap/providers.php), no panel change is planned, no global-search change is planned, and no new assets are expected.
|
||||
|
||||
**Gate evaluation**: PASS.
|
||||
|
||||
- The narrow path is defensible if the implementation keeps package delivery anchored to the current released review and current review-pack download seam.
|
||||
- The plan fails the gate if it drifts into new package persistence, scheduled generation, a new report engine, or a new stored-report viewer shell.
|
||||
|
||||
**Post-design re-check**: PASS once [research.md](research.md), [data-model.md](data-model.md), [quickstart.md](quickstart.md), and [contracts/governance-service-packaging.openapi.yaml](contracts/governance-service-packaging.openapi.yaml) are present and the agent-context refresh step is executed.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Unit for any review-summary composition adjustments in the existing tenant-review composer seam; Feature for workspace readiness, released-review detail package summary, package/download availability, audit metadata, truth separation, and deny-as-not-found or in-scope capability boundaries; Browser for one bounded workspace-to-detail-to-package smoke path
|
||||
- **Affected validation lanes**: confidence, browser
|
||||
- **Why this lane mix is the narrowest sufficient proof**: the repo already has focused unit and feature files covering the exact review, pack, evidence, and audit seams this feature will reuse, and one existing browser smoke is enough to catch rendered disclosure and action-hierarchy regressions without creating a new heavy family
|
||||
- **Narrowest proving command(s)**:
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantReview/TenantReviewComposerTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewAuditLogTest.php tests/Feature/TenantReview/TenantReviewExecutivePackTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||
- **Fixture / helper / factory / seed / context cost risks**: moderate but contained; reuse existing workspace membership, released review, current export review pack, evidence snapshot, stored-report-backed evidence items, finding exception decision, and audit fixtures
|
||||
- **Expensive defaults or shared helper growth introduced?**: no; any new helper should stay explicit and local to the review/review-pack 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 detail and package reuse
|
||||
- **Closing validation and reviewer handoff**: rerun the commands above, verify the package path never starts export generation, verify current review-pack reuse and signed download behavior, verify accepted-risk or exception truth stays narrowed to current governance decisions, verify missing supporting access resolves as calm unavailable state or direct-route denial, and verify out-of-scope targets stay `404`
|
||||
- **Budget / baseline / trend follow-up**: none expected beyond small feature-local additions in the existing suites
|
||||
- **Review-stop questions**: lane fit, hidden fixture growth, accidental run generation, stored-report viewer drift, packaging abstraction creep
|
||||
- **Escalation path**: `document-in-feature` for contained metadata or copy notes; `follow-up-spec` for a real stored-report viewer or broader package-profile need; `reject-or-split` for any drift into new package persistence, report engines, schedules, or campaigns
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage
|
||||
- **Why no dedicated follow-up spec is needed**: this feature is already the bounded packaging follow-up after Specs 258 and 259. Only broader profile automation, stored-report viewer productization, or multi-tenant packaging would justify a separate follow-up
|
||||
|
||||
## Rollout & Risk Controls
|
||||
|
||||
- Keep the canonical entry surface on the existing customer review workspace and the canonical secondary surface on the existing released-review detail route.
|
||||
- Keep package delivery anchored to current released-review truth and current review-pack reuse. Do not trigger `exportExecutivePackAction()` or `ReviewPackService::generateFromReview()` from the customer-workspace path.
|
||||
- Treat `publishable` review-pack truth as `available`, stale or internal-only but still-readable truth as `partial`, missing or blocked source truth as `unavailable`, historical-only truth as `expired`, and package-level entitlement restriction as `blocked`. If only supporting proof is restricted, keep the package summary readable and mark the affected supporting link unavailable or forbidden instead. Do not generate missing truth and do not imply calmness where the source basis is incomplete.
|
||||
- Keep branding and profile variants deferred from v1. Neutral governance-package framing must not introduce a new presentation-truth source, layout system, or profile configuration seam.
|
||||
- Keep stored-report detail subordinate. If implementation cannot point to an existing entitled viewer seam, the package should stop at calm evidence-basis disclosure rather than inventing a new route.
|
||||
- Keep global search posture, provider registration, and asset strategy unchanged.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/260-governance-service-packaging/
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ └── governance-service-packaging.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
|
||||
│ ├── Http/Controllers/
|
||||
│ │ └── ReviewPackDownloadController.php
|
||||
│ ├── Models/
|
||||
│ │ ├── TenantReview.php
|
||||
│ │ ├── ReviewPack.php
|
||||
│ │ ├── EvidenceSnapshot.php
|
||||
│ │ ├── StoredReport.php
|
||||
│ │ └── FindingException.php
|
||||
│ ├── Services/
|
||||
│ │ ├── Audit/WorkspaceAuditLogger.php
|
||||
│ │ ├── Evidence/Sources/
|
||||
│ │ │ ├── EntraAdminRolesSource.php
|
||||
│ │ │ └── PermissionPostureSource.php
|
||||
│ │ ├── ReviewPackService.php
|
||||
│ │ └── TenantReviews/
|
||||
│ │ ├── TenantReviewComposer.php
|
||||
│ │ └── TenantReviewSectionFactory.php
|
||||
│ ├── Support/
|
||||
│ │ ├── Audit/AuditActionId.php
|
||||
│ │ ├── Auth/Capabilities.php
|
||||
│ │ ├── Governance/Controls/ComplianceEvidenceMappingV1.php
|
||||
│ │ └── Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
|
||||
├── bootstrap/providers.php
|
||||
├── lang/
|
||||
│ ├── de/localization.php
|
||||
│ └── en/localization.php
|
||||
├── resources/views/filament/pages/reviews/customer-review-workspace.blade.php
|
||||
└── tests/
|
||||
├── Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php
|
||||
├── Feature/Evidence/
|
||||
├── Feature/ReviewPack/
|
||||
├── Feature/Reviews/
|
||||
├── Feature/TenantReview/
|
||||
└── Unit/TenantReview/TenantReviewComposerTest.php
|
||||
```
|
||||
|
||||
**Structure Decision**: Laravel monolith. The implementation should stay inside the existing `apps/platform` review, review-pack, evidence, audit, localization, and shared-truth seams, with no new panel/provider location and no new persistence layer.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| None expected at planning time | The intended implementation is a bounded product contract over existing review, pack, evidence, and audit seams | Adding a `GovernancePackage` model, report engine, schedule, campaign flow, or new viewer shell would import unnecessary permanent complexity |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: operators still have to assemble stakeholder-ready governance packages manually outside the product, which risks drift from shared interpretation and artifact truth.
|
||||
- **Existing structure is insufficient because**: current review workspace and released-review detail surfaces explain one released review, but they do not yet present one explicit management-ready package contract over the same truth and current package-download seam.
|
||||
- **Narrowest correct implementation**: tighten the existing workspace and released-review detail flow so package readiness, summary, and current review-pack download reuse are explicit and consistent, while leaving generation, scheduling, and broader export orchestration out of scope.
|
||||
- **Ownership cost created**: bounded copy and disclosure maintenance on existing surfaces, focused audit-metadata reuse, and small expansions in existing test files.
|
||||
- **Alternative intentionally rejected**: a standalone `GovernancePackage` model, a package-specific report engine, a customer portal, a stored-report viewer shell, or scheduled campaign packaging were rejected because the repo already has the required review, interpretation, review-pack, evidence, and audit seams for a smaller v1.
|
||||
- **Release truth**: current-release commercial packaging follow-through after Specs 258 and 259, 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 route as the only primary and secondary surfaces
|
||||
- prefer current review-pack reuse and signed download over creating a new exported artifact or starting generation from the management-ready path
|
||||
- derive package summary from existing review summary and section truth rather than a parallel package record or report engine
|
||||
- keep `open decisions` narrowed to accepted-risk and exception decision truth only
|
||||
- reuse artifact-truth seams for availability, expiry, and artifact/result truth separation
|
||||
- treat stored reports as subordinate source truth and explicitly defer inventing a new viewer where the repo does not already expose one
|
||||
- reuse existing audit events and metadata rather than introducing a new audit family
|
||||
- keep validation inside existing unit/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 packaging shape:
|
||||
|
||||
- no new table, package artifact family, or report namespace; package truth remains derived from existing reviews, sections, packs, evidence snapshots, stored reports, and governance decisions
|
||||
- one conceptual package-summary contract documents the derived management-ready payload without prescribing new persistence
|
||||
- the conceptual contract documents the existing workspace, released-review detail, review-pack download, and evidence routes reused by this feature
|
||||
- quickstart records the intended implementation order, targeted tests, Filament v5 plus Livewire v4 posture, unchanged provider-registration location, unchanged global-search posture, unchanged destructive-action posture for the package path, and unchanged asset strategy
|
||||
- `.specify/scripts/bash/update-agent-context.sh copilot` must run after the design artifacts are generated, even if it results in no technology additions
|
||||
|
||||
**Artifacts**:
|
||||
|
||||
- [data-model.md](data-model.md)
|
||||
- [contracts/governance-service-packaging.openapi.yaml](contracts/governance-service-packaging.openapi.yaml)
|
||||
- [quickstart.md](quickstart.md)
|
||||
|
||||
## Phase 2 — Planning (for tasks.md)
|
||||
|
||||
Dependency-ordered outline for the later `tasks.md` step:
|
||||
|
||||
1. Tighten the workspace page and intro copy so package readiness and management value are visible without adding a competing package row action.
|
||||
2. Tighten the released-review detail flow in customer-workspace mode so it owns the management-ready package summary and keeps only one dominant package action.
|
||||
3. Reuse the current review summary, interpretation, risk, and section seams to keep package wording aligned across workspace, detail, and supporting artifact access.
|
||||
4. Reuse current review-pack truth, signed download generation, and artifact-truth presenter output for availability, expired, blocked, and partial states.
|
||||
5. Keep stored-report-backed evidence subordinate to the package summary, surfacing explicit unavailable or secondary access states rather than inventing a new viewer.
|
||||
6. Reuse the shared audit path and current capabilities, adding only bounded metadata or assertions if the current events do not already cover the required moments.
|
||||
7. Expand the focused review, review-pack, evidence, and audit suites plus the single browser smoke, without introducing a new heavy family.
|
||||
|
||||
## 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 new destructive action is introduced on the package path, and no new asset bundle is planned.
|
||||
- Shared-path result: the plan keeps package meaning and availability on existing review, interpretation, review-pack, evidence, and artifact-truth seams rather than a new package engine.
|
||||
- Stored-report tension result: the repo clearly has stored-report-backed evidence inputs but no obvious shared stored-report viewer resource in the current operator plane. The plan therefore keeps stored reports as subordinate source truth and defers any new viewer shell unless implementation proves an existing viewer seam already exists.
|
||||
- Preparation workflow result: the existing [checklists/requirements.md](checklists/requirements.md) already carries the required review outcome and workflow outcome conventions, so no new checklist was needed during this plan pass.
|
||||
53
specs/260-governance-service-packaging/quickstart.md
Normal file
53
specs/260-governance-service-packaging/quickstart.md
Normal file
@ -0,0 +1,53 @@
|
||||
# Quickstart — Governance-as-a-Service Packaging v1
|
||||
|
||||
## Preconditions
|
||||
|
||||
- Docker is running and the Sail stack for `apps/platform` is available.
|
||||
- The feature stays inside the existing Laravel monolith and current 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 `GovernancePackage` persistence family, no new report engine, no new schedule or batch path, no new `OperationRun`, no new stored-report viewer shell, no global-search expansion, and no asset strategy change are in scope.
|
||||
- The management-ready path must reuse released-review truth plus existing review-pack, evidence, stored-report-backed evidence, and governance-decision truth.
|
||||
|
||||
## Intended Implementation Order
|
||||
|
||||
1. Review the current workspace, released-review detail, tenant-review composition, review-pack download, artifact-truth, evidence-source, capability, audit, and localization seams so the implementation stays on one shared path.
|
||||
2. Confirm that package readiness on the workspace can be expressed from current review summary and artifact-truth semantics without introducing a competing package row action.
|
||||
3. Confirm that the released-review detail surface in customer-workspace mode remains read-only and continues to expose only the current signed package-download seam instead of starting export generation.
|
||||
4. Tighten [../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php](../../apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php) and its Blade intro so package readiness, management value, evidence basis, and calm unavailable states are visible at the right disclosure level.
|
||||
5. Tighten [../../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 owns the management-ready summary and keeps one dominant package action.
|
||||
6. Reuse existing `TenantReview.summary`, section payloads, and [../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php](../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php) for package wording instead of adding a package-local mapper.
|
||||
7. Reuse [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) plus current review-pack truth for `available`, `partial`, `unavailable`, `expired`, and `blocked` states rather than a new package state family. Use the canonical mapping `publishable -> available`, `internal_only` or `stale -> partial`, `blocked` or `missing_input -> unavailable`, `historical_only -> expired`, and package-level entitlement restrictions -> `blocked`. If only supporting proof is restricted, keep the package summary readable and mark the affected secondary link unavailable or forbidden. Stale or entitlement-restricted conditions must map to reason codes inside those states, not to a second availability taxonomy.
|
||||
8. Keep stored reports subordinate to evidence basis; only link to secondary detail when an existing entitled viewer seam already exists.
|
||||
9. Reuse current audit events and current capability checks for package access, package download, and proof access.
|
||||
10. Expand only the existing unit, feature, and smoke suites listed below, then run the targeted tests and Pint.
|
||||
|
||||
## Targeted Validation Commands (after implementation)
|
||||
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantReview/TenantReviewComposerTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewAuditLogTest.php tests/Feature/TenantReview/TenantReviewExecutivePackTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.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 an actor with workspace scope and open `/admin/reviews/workspace`.
|
||||
2. Confirm only entitled tenants appear and that package readiness is shown as information, not as a second competing row action.
|
||||
3. Confirm the workspace exposes calm management-ready cues built from current review truth, including current evidence basis and current recommended next action.
|
||||
4. Open a released review and confirm the detail surface owns the package summary and exposes the current package action without showing operator-only export actions.
|
||||
5. Confirm the package path reuses current review-pack truth and does not trigger generation or a new `OperationRun` when supporting artifacts are missing.
|
||||
6. Confirm accepted-risk entries stay distinct from governance-decision follow-up entries, remain bounded to current governance truth, and do not read like a broader decision inbox.
|
||||
7. Drill into supporting proof where entitled and confirm raw payloads and support-only diagnostics remain secondary.
|
||||
8. Attempt an out-of-scope tenant or review target and confirm the response remains not found without leaking package, review, or artifact presence.
|
||||
|
||||
## Notes
|
||||
|
||||
- This is a preparation-only package. No application implementation or validation results belong in this planning artifact yet.
|
||||
- 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, remediation, publication, generation, or provider-changing action belongs on the management-ready package path.
|
||||
- No branding, profile-variant, or client-specific layout system belongs in v1; `Download governance package` remains neutral framing over the current export review pack only.
|
||||
- No new Filament assets are expected. If later implementation unexpectedly registers assets, deployment still requires `cd apps/platform && php artisan filament:assets`, but this package does not plan such a change.
|
||||
- The plan assumes current review-pack reuse is the default delivery seam. If implementation proves that no current pack exists for an otherwise eligible released review, the correct v1 behavior is an explicit unavailable or partial state, not a new generation workflow.
|
||||
- Stored reports stay subordinate source truth. If no current entitled viewer seam exists, the package should stop at truthful evidence-basis disclosure rather than inventing a new route.
|
||||
141
specs/260-governance-service-packaging/research.md
Normal file
141
specs/260-governance-service-packaging/research.md
Normal file
@ -0,0 +1,141 @@
|
||||
# Research — Governance-as-a-Service Packaging v1
|
||||
|
||||
**Date**: 2026-05-01
|
||||
**Spec**: [spec.md](spec.md)
|
||||
|
||||
This document resolves the planning decisions for the smallest safe management-ready package over one released review context.
|
||||
|
||||
## 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) in customer-workspace mode as the only secondary context surface.
|
||||
|
||||
**Rationale**:
|
||||
- The repo already has the admin-plane route, tenant-safe prefilter behavior, package-readiness-adjacent columns, and bounded feature and browser coverage for these surfaces.
|
||||
- The missing product contract is management-ready packaging over existing truth, not new routing, a new panel, or a customer portal shell.
|
||||
- Reusing these surfaces keeps the package anchored to one released review context and prevents a second package registry or dashboard.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add a dedicated package page or customer-only package portal.
|
||||
- Rejected: duplicates the current review-consumption path and widens scope into shell-level IA and identity concerns.
|
||||
- Put all management-ready content on the workspace only.
|
||||
- Rejected: would overload the list surface and weaken the existing released-review drilldown as the package-owning context.
|
||||
|
||||
## Decision 2 — Prefer current review-pack reuse and signed download over creating a new exported artifact or generation path
|
||||
|
||||
**Decision**: Treat the current export review pack and its signed download route as the default package-delivery seam for v1. The management-ready path should reuse `downloadCurrentReviewPackAction()` and current review-pack truth before considering any new artifact concept.
|
||||
|
||||
**Rationale**:
|
||||
- In customer-workspace mode [../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php](../../apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php) already exposes `downloadCurrentReviewPackAction()` and suppresses operator lifecycle actions.
|
||||
- [../../apps/platform/app/Services/ReviewPackService.php](../../apps/platform/app/Services/ReviewPackService.php) already provides signed download URLs, while `generateFromReview()` is tied to the existing export workflow and `OperationRun` lifecycle.
|
||||
- The feature spec explicitly forbids package generation runs and a new package artifact family. Reusing current review-pack truth is therefore the narrowest correct path.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add a new `GovernancePackage` artifact or export namespace.
|
||||
- Rejected: creates parallel persistence and new lifecycle semantics the spec explicitly forbids.
|
||||
- Start `generateFromReview()` from the management-ready package action when no pack exists.
|
||||
- Rejected: introduces a queued generation path and breaks the explicit no-`OperationRun` scope boundary.
|
||||
|
||||
## Decision 3 — Derive package meaning from current review summary and section truth, not from a parallel package record
|
||||
|
||||
**Decision**: Compose package meaning from existing released-review summary and section payloads, especially `summary.control_interpretation`, `summary.highlights`, and the current section family created by [../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php](../../apps/platform/app/Services/TenantReviews/TenantReviewComposer.php) and [../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php](../../apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php).
|
||||
|
||||
**Rationale**:
|
||||
- The repo already persists the management-relevant building blocks needed for this slice inside the released review artifact.
|
||||
- Existing sections already cover executive summary, control interpretation, open risks, accepted risks, permission posture, baseline drift, and operations health.
|
||||
- Reusing those payloads keeps workspace, detail, and package access on one truth path and avoids a second semantic layer.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Persist a package-specific JSON blob or summary projection.
|
||||
- Rejected: adds a new source of truth for content already represented by the released review.
|
||||
- Compose package content directly from raw findings, evidence payloads, and stored reports in page code.
|
||||
- Rejected: bypasses the shared interpretation path and increases drift risk.
|
||||
|
||||
## Decision 4 — Keep Spec 259’s shared interpretation layer mandatory and keep “open decisions” narrowed to risk-acceptance / exception truth
|
||||
|
||||
**Decision**: Package content must remain dependent on [../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php](../../apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php) and on the review’s existing `control_interpretation` payload. The roadmap phrase `open decisions` remains narrowed to accepted-risk and exception decision truth only.
|
||||
|
||||
**Rationale**:
|
||||
- The spec requires the package to stay on the shared customer-safe interpretation path and explicitly forbids raw finding or report language as the package’s meaning layer.
|
||||
- `ComplianceEvidenceMappingV1` already includes accepted-risk follow-up wording and limitation semantics that fit the package’s calm disclosure goal.
|
||||
- Narrowing `open decisions` to existing exception and risk-acceptance truth keeps scope out of queue or workflow-engine territory.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Invent a package-local management summary mapper.
|
||||
- Rejected: would fork the current interpretation contract and create inconsistent wording across workspace, detail, and package access.
|
||||
- Generalize `open decisions` into a broader governance inbox or approval board.
|
||||
- Rejected: imports a new workflow engine and wider product semantics beyond the current slice.
|
||||
|
||||
## Decision 5 — Reuse artifact-truth seams for availability, expiry, and truth separation
|
||||
|
||||
**Decision**: Reuse [../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php](../../apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php) for current review, evidence snapshot, and review-pack availability or expiry semantics, and keep artifact/result truth separation explicit in the package contract.
|
||||
|
||||
**Rationale**:
|
||||
- `ArtifactTruthPresenter` already resolves envelopes for `TenantReview`, `EvidenceSnapshot`, and `ReviewPack` and already carries compressed truth about artifact existence, readiness, and reasons.
|
||||
- The package spec explicitly requires artifact/result truth separation: the package summary must not redefine review, pack, evidence, or report truth.
|
||||
- Reusing the presenter keeps package availability semantics aligned with adjacent governance artifact surfaces.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add a new package availability enum or presenter family.
|
||||
- Rejected: redundant with current artifact-truth seams and disproportionate for this slice.
|
||||
- Collapse review truth, review-pack truth, and evidence truth into one package status.
|
||||
- Rejected: violates the repo’s operator-surface guidance around distinct truth dimensions.
|
||||
|
||||
## Decision 6 — Keep stored reports subordinate source truth and do not invent a new viewer shell during v1 planning
|
||||
|
||||
**Decision**: Treat stored reports as existing evidence inputs and secondary drilldown only where the repo already exposes them through current seams. Do not introduce a new stored-report viewer route or shell during v1 planning.
|
||||
|
||||
**Rationale**:
|
||||
- The repo clearly has stored-report-backed evidence inputs through seams such as [../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php](../../apps/platform/app/Services/Evidence/Sources/PermissionPostureSource.php) and [../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php](../../apps/platform/app/Services/Evidence/Sources/EntraAdminRolesSource.php).
|
||||
- A shared stored-report viewer resource is not obvious in the current operator plane, so inventing one here would exceed the slice and introduce a new surface family.
|
||||
- The package can still remain truthful by summarizing stored-report-backed evidence basis and marking supporting detail unavailable when no existing entitled viewer seam exists.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Create a new stored-report detail surface as part of packaging.
|
||||
- Rejected: adds a new viewer family not required to ship one management-ready package.
|
||||
- Ignore stored-report-backed evidence entirely.
|
||||
- Rejected: would understate the evidence basis and make the package less faithful to the existing review truth.
|
||||
|
||||
## Decision 7 — Reuse current audit events and metadata instead of introducing a new audit family
|
||||
|
||||
**Decision**: Keep package auditability on the existing `customer_review_workspace.opened`, `tenant_review.opened`, `review_pack.downloaded`, and `evidence_snapshot.opened` events, enriching metadata before introducing any new audit concept.
|
||||
|
||||
**Rationale**:
|
||||
- [../../apps/platform/app/Support/Audit/AuditActionId.php](../../apps/platform/app/Support/Audit/AuditActionId.php) already defines the relevant action IDs for the package path.
|
||||
- Workspace and released-review entry points already record interpretation-version and source-surface context, and review-pack downloads already flow through a shared signed route.
|
||||
- The spec requires auditability, not a new audit store or family.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Create a new `governance_package.opened` event family up front.
|
||||
- Rejected: not justified until the feature proves it needs an explicit open event beyond current review-open and pack-download moments.
|
||||
- Rely on passive render without audit coverage.
|
||||
- Rejected: weakens attributable access semantics for a stakeholder-ready deliverable.
|
||||
|
||||
## Decision 8 — Defer branding and profile variants from v1 and keep neutral localization-ready package framing
|
||||
|
||||
**Decision**: V1 does not introduce MSP branding, profile variants, or client-specific layouts. `Download governance package` remains neutral governance-package framing over the current export review pack for the released review, and new labels stay in the existing DE/EN localization posture.
|
||||
|
||||
**Rationale**:
|
||||
- Repo exploration did not reveal a clear existing branding source-of-truth that this slice could safely reuse without hidden configuration or persistence work.
|
||||
- The spec only needs one repeatable management-ready package path; it does not need brand customization to prove product fit.
|
||||
- Deferring branding avoids accidental profile-variant, layout, or package-template framework growth.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add bounded package branding now.
|
||||
- Rejected: no clear repo-real branding source-of-truth was confirmed, so this would import hidden scope.
|
||||
- Hardcode package wording inline on page classes.
|
||||
- Rejected: weakens localization-readiness and makes cross-surface wording drift more likely.
|
||||
|
||||
## Decision 9 — Keep validation in existing review, review-pack, evidence, and smoke test families
|
||||
|
||||
**Decision**: Expand the existing unit, feature, and single browser smoke files that already cover the review, pack, evidence, and audit seams touched by this feature.
|
||||
|
||||
**Rationale**:
|
||||
- The repo already has focused tests for workspace behavior, released-review explanation, review-pack download, entitlement enforcement, evidence audit logging, and the bounded workspace smoke path.
|
||||
- Those files are the cheapest honest proof for package reuse, scope isolation, calm unavailable states, and truth separation.
|
||||
- Reusing them honors the spec’s test-governance requirement to avoid a broader heavy family.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Add a new package-specific browser suite.
|
||||
- Rejected: too expensive for the bounded value of this slice.
|
||||
- Rely only on unit tests around new helpers.
|
||||
- Rejected: the feature’s core contract lives on rendered Filament surfaces and needs surface-level proof.
|
||||
400
specs/260-governance-service-packaging/spec.md
Normal file
400
specs/260-governance-service-packaging/spec.md
Normal file
@ -0,0 +1,400 @@
|
||||
# Feature Specification: Governance-as-a-Service Packaging v1
|
||||
|
||||
**Feature Branch**: `260-governance-service-packaging`
|
||||
**Created**: 2026-05-01
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Prepare Governance-as-a-Service Packaging v1 as the smallest viable on-demand management-ready governance package for one released review context, built from existing review packs, evidence snapshots, stored reports, findings, accepted-risk truth, and the shared compliance interpretation layer, without adding a new panel, report engine, scheduling system, or parallel package domain."
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: TenantPilot already has repo-real tenant reviews, review packs, evidence snapshots, stored reports, accepted-risk governance, and customer-safe review surfaces, but operators still lack one repeatable management-ready deliverable that packages that truth for MSP and customer stakeholders.
|
||||
- **Today's failure**: Operators still rely on manual screenshot decks, spreadsheet extracts, or ad-hoc summaries to explain released review truth. That manual packaging can bypass the customer-safe interpretation layer, weaken auditability, and drift away from the underlying review and artifact truth.
|
||||
- **User-visible improvement**: An entitled actor can open one released review context and access one repeatable management-ready governance package that is customer-safe and management-readable by default.
|
||||
- **Smallest enterprise-capable version**: One on-demand governance package for one released review context inside the current admin-plane review flow, derived from existing released review, review-pack, evidence, stored-report, finding, and exception or risk-acceptance truth plus the shared customer-safe interpretation layer from Spec 259. No new portal, no scheduling, no multi-pack automation, and no second source of truth ship in v1.
|
||||
- **Explicit non-goals**: No new panel, no customer portal, no separate identity plane, no new report engine, no scheduling or batching system, no campaign workflow, no CRM or PSA handoff, no AI-generated summaries, no raw operator-data dump as the default deliverable, no broad multi-pack automation, and no standalone `GovernancePackage` domain.
|
||||
- **Permanent complexity imported**: One bounded management-ready packaging contract over existing review and export surfaces, one bounded package-availability state on current review surfaces, focused feature plus bounded browser coverage, and explicit audit expectations for package access. No new table, no new panel, no new identity plane, and no campaign framework are introduced.
|
||||
- **Why now**: `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, and `docs/product/implementation-ledger.md` all keep this as an active open commercial follow-up, and Specs 258 and 259 explicitly defer it as the next bounded step after customer-safe review productization and shared interpretation are prepared.
|
||||
- **Why not local**: A one-off PDF or export tweak or a single-page summary would either bypass the shared interpretation and artifact truth or remain too inconsistent across review, evidence, and review-pack surfaces to become a repeatable MSP deliverable.
|
||||
- **Approval class**: Core Enterprise
|
||||
- **Red flags triggered**: New packaging vocabulary and multi-surface reuse. Defense: the slice remains fully derived from existing review, export, evidence, and interpretation truth; it introduces no new persistence or automation framework; it explicitly narrows the roadmap phrase `open decisions` to repo-real exception and risk-acceptance decision truth; and it defers branding or profile variants rather than inventing a new presentation-truth source.
|
||||
- **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 review-pack access or download surface reached from released review context
|
||||
- existing evidence summary and proof routes reached from released review context when the actor is entitled
|
||||
- **Data Ownership**: All visible truth remains derived from existing tenant-owned `TenantReview`, `ReviewPack`, `EvidenceSnapshot`, `StoredReport`, `Finding`, `FindingException`, `FindingExceptionDecision`, and `AuditLog` records plus the shared customer-safe interpretation layer prepared by Spec 259. No new `GovernancePackage` table, no mirrored package artifact family, no campaign record, and no parallel report-store namespace are introduced. In v1, the downloadable artifact behind `Download governance package` is the current export review pack for the released review; the governance package summary is derived presentation over that existing artifact and related evidence truth, not a second export family. If an existing review or export artifact carries package output, it remains subordinate to the released review context and does not become independent source truth.
|
||||
- **RBAC**:
|
||||
- this remains an admin-plane follow-up, not a new panel or authorization plane
|
||||
- workspace membership remains the first isolation boundary
|
||||
- page entry requires an established workspace scope plus at least one entitled tenant and released review the actor may read through the current capability registry
|
||||
- package access reuses existing review visibility, review-pack access, evidence or proof access, workspace entitlements, and current audit boundaries
|
||||
- non-members and out-of-scope tenant or review targets resolve as deny-as-not-found
|
||||
- actors inside an established scope may receive capability denial on the reused released-review detail route if they lack inherited review-view capability, and on gated secondary artifact or proof paths they are not entitled to use
|
||||
- the package remains read-only and introduces no operator mutation or provider-changing action
|
||||
|
||||
For canonical-view specs, the spec MUST define:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: When launched from tenant-scoped review, evidence, or review-pack 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 package availability, released-review entry, review-pack access, evidence links, and any already-existing secondary proof drilldowns only resolve for tenants and released reviews the actor is already entitled to read in the current workspace. Inaccessible 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, management summary sections, action links, evidence viewers, review-pack access, package-availability states, and stored-report-backed evidence disclosure
|
||||
- **Systems touched**: existing `CustomerReviewWorkspace`, existing released review detail surfaces, `TenantReviewSectionFactory`, `TenantReviewComposer`, the shared `control_interpretation` contract prepared by Spec 259, `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, existing review-pack access or download surfaces, existing evidence summary and proof viewers, stored-report-backed evidence inputs, localization, and audit infrastructure
|
||||
- **Existing pattern(s) to extend**: the current customer review productization contract from Spec 258, the shared customer-safe control or readiness interpretation path from Spec 259, and the existing review-pack, evidence, and artifact-truth disclosure surfaces
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: `TenantReviewSectionFactory`, `TenantReviewComposer`, the shared `control_interpretation` contract from Spec 259, `ArtifactTruthPresenter`, `ArtifactTruthEnvelope`, `SurfaceCompressionContext`, and the current review, evidence, stored-report-backed evidence, and review-pack disclosure surfaces
|
||||
- **Why the existing shared path is sufficient or insufficient**: Existing review, interpretation, review-pack, evidence, stored-report, and audit truth already cover what the package should say. The missing piece is one repeatable management-ready packaging pass that keeps those truths aligned instead of forcing manual translation or page-local exports.
|
||||
- **Allowed deviation and why**: none. This slice must tighten the current review and export path rather than introduce a parallel package domain, package-specific vocabulary layer, or direct raw-report export.
|
||||
- **Consistency impact**: Executive summary wording, top findings, accepted-risk governance wording, governance-decision follow-up phrasing, evidence-link language, package-availability states, current review-pack framing, and audit labels must stay aligned between workspace summary, released review detail, and package access.
|
||||
- **Review focus**: Reviewers must block any new standalone package domain, any page-local interpretation path, any raw data-dump default, or any management export that bypasses released review and shared interpretation truth.
|
||||
|
||||
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: no
|
||||
- **Shared OperationRun UX contract/layer reused**: `N/A`
|
||||
- **Delegated start/completion UX behaviors**: `N/A`
|
||||
- **Local surface-owned behavior that remains**: This slice packages only already released review truth and already available evidence, report, and review-pack inputs. It must not start new review-pack, stored-report, evidence, or package runs in v1.
|
||||
- **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**: customer-safe interpretation reuse, management package wording, evidence-link semantics, package labels, and summary language
|
||||
- **Neutral platform terms preserved or introduced**: governance package, released review, control or readiness summary, evidence basis, accepted-risk governance decision, and review context
|
||||
- **Provider-specific semantics retained and why**: Provider-specific report types or evidence labels may remain reachable only through entitled secondary drilldown because some existing stored reports are provider-owned artifacts. They are not part of the default management-ready package language.
|
||||
- **Why this does not deepen provider coupling accidentally**: Package summary and availability are derived from released review truth plus the shared customer-safe interpretation layer, not from raw Microsoft report types, provider identifiers, or provider payload language.
|
||||
- **Follow-up path**: future framework-specific package profiles or broader portfolio packaging remain follow-up specs after this bounded v1 proves itself
|
||||
|
||||
## 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 and evidence primitives | status messaging, package availability, navigation entry points | page, table or filter state, disclosure state | no | Existing page stays the selection surface and does not become a separate package registry |
|
||||
| Released Customer Review detail | yes | Native Filament resource or detail surface plus shared review, evidence, and export primitives | management summary sections, package access, evidence and report viewers | detail sections, disclosure state, access states | no | Existing detail becomes the package-owning context and avoids a new page shell |
|
||||
|
||||
## 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 | Operator decides which released review context is ready for stakeholder packaging | released review state, package availability, top-line governance signal, and evidence-basis presence | released review detail, package summary, and supporting proof after explicit open | Primary because it is the calm review-selection surface and should answer where packaging can begin before deeper inspection | Aligns packaging with the existing review-consumption workflow instead of with storage-object navigation | Removes hunting across review pack, stored report, and evidence screens before the first decision |
|
||||
| Released Customer Review detail | Secondary Context Surface | Operator confirms what the package will say and accesses the package for one released review | executive summary, top findings, accepted risks, governance-decision follow-up, evidence links, and package state | deeper proof, stored-report context, and raw or support detail only after explicit drilldown | Secondary because it is entered after the workspace has already selected one review context | Keeps packaging centered on one released review instead of inventing a second package dashboard | Removes manual summary assembly by making the released review the single packaging anchor |
|
||||
|
||||
## 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 | operator-MSP, customer-admin, auditor-read-only, customer-read-only | released review state, package availability, calm executive-summary teaser, top findings and accepted-risk signal, and evidence-basis presence | review freshness, lineage, and source completeness only after opening the review | raw reports, provider IDs, fingerprints, and raw evidence remain hidden or gated | `Open released review` | raw or support detail and secondary artifact links stay off the list surface by default | Workspace states package readiness once; detail owns the actual package summary |
|
||||
| Released Customer Review detail | operator-MSP, customer-admin, auditor-read-only, customer-read-only | management-ready executive summary, top findings, accepted risks, current governance decisions needing awareness, evidence links, package scope, current review-pack delivery framing, and package-access state | review lineage, freshness, section completeness, and source artifact detail in secondary sections only | raw evidence payloads, stored-report payloads, provider-debug context, and unrestricted support detail remain hidden or gated | `Download governance package` | raw source payloads, unrestricted stored-report detail, and operator-only diagnostics remain secondary and explicit | Detail owns the management package summary; deeper proof links add source context without restating the same summary cards as peer 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 packaging context | full-row open to released review detail | required | no peer package row action; package availability stays informational on the list surface | none | `/admin/reviews/workspace` | `/admin/t/{tenant}/reviews/{record}` | workspace context, tenant prefilter, release state, package availability | Customer review | whether the current released review is eligible for management-ready delivery | none |
|
||||
| Released Customer Review detail | Detail / Report / Management export | Read-only detail report | Download the governance package for the current released review | sectioned detail page with one dominant safe header action | forbidden | existing proof links and review-pack access remain secondary in-body or lower-priority actions | none | `/admin/reviews/workspace` | `/admin/t/{tenant}/reviews/{record}` | workspace, tenant, release state, interpretation version, package availability | Governance package | what this package covers, what matters now, and which evidence basis 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 | MSP operator or entitled customer-safe reviewer | Decide whether the current released review is ready to package and which review to open | Read-only workspace review overview | Which released review should I package for stakeholder delivery? | released review state, package availability, top-line findings or accepted-risk signal, and evidence-basis presence | review lineage, freshness, and source completeness in secondary context only | release state, package readiness, evidence completeness, accepted-risk governance signal | none | Open released review | none |
|
||||
| Released Customer Review detail | MSP operator or entitled customer-safe reviewer | Inspect the management-ready summary and access the package for one released review | Read-only detail report | What will this stakeholder package say, and is the supporting basis ready and safe to deliver? | executive summary, top findings, accepted risks, governance-decision follow-up, evidence links, package state, and explicit current review-pack delivery framing | deeper stored-report and evidence provenance plus raw or support details only after explicit drilldown and entitlement | interpretation version, evidence sufficiency, accepted-risk governance validity, package availability | none | Download governance package | none |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no new artifact family
|
||||
- **New abstraction?**: no
|
||||
- **New enum/state/reason family?**: no
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: Existing review truth can explain one released review, but it still does not provide one repeatable management-ready package that stays aligned with shared interpretation and artifact truth.
|
||||
- **Existing structure is insufficient because**: Review workspace and detail flows answer review questions, but they do not yet provide a bounded deliverable contract for stakeholder packaging. Manual packaging currently re-translates the same truth outside the product.
|
||||
- **Narrowest correct implementation**: One derived management-ready package bound to one released review context and current review, evidence, report, and export surfaces, with explicit unavailable states instead of generating missing source truth.
|
||||
- **Ownership cost**: A bounded disclosure and wording pass plus focused package-access, authorization, and audit tests, and one bounded browser smoke.
|
||||
- **Alternative intentionally rejected**: A standalone `GovernancePackage` model and campaign engine were rejected as structurally too heavy. A page-local export template that reads raw findings or reports directly was rejected because it would drift from shared interpretation and artifact truth.
|
||||
- **Release truth**: current-release commercial follow-through, not future-release platform buildout
|
||||
|
||||
### 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 reuse of existing review and export truth is preferred over introducing a new package domain.
|
||||
|
||||
## 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 package availability logic, interpretation dependency, auditability, `404` or `403` boundaries, explicit unavailable or partial states, and truth separation across the existing review surfaces. One bounded browser smoke remains justified because this slice materially changes default-visible management-ready content and action hierarchy on rendered review surfaces.
|
||||
- **New or expanded test families**: expand the existing `Reviews/CustomerReviewWorkspace`, `TenantReview`, `ReviewPack`, and adjacent audit or package-access families; keep exactly one bounded browser smoke around the current review workspace and detail flow
|
||||
- **Fixture / helper cost impact**: low to moderate. Reuse existing workspace membership, released review, review pack, evidence snapshot, stored report, finding, exception decision, interpretation summary, and audit fixtures. Avoid provider sync or queue-heavy defaults.
|
||||
- **Heavy-family visibility / justification**: exactly one browser smoke stays explicit because the slice is primarily about packaging disclosure and action placement on existing rendered surfaces. No broader browser or 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, availability, localization-ready wording, and package-truth separation. The bounded browser smoke is the only required rendered proof.
|
||||
- **Reviewer handoff**: Reviewers must confirm that package access never bypasses the shared interpretation layer, no new package domain or search surface appears, out-of-scope tenants leak nothing, management summary stays customer-safe, `Download governance package` resolves to the current export review pack rather than a second artifact family, and no branding or profile system is smuggled into v1.
|
||||
- **Budget / baseline / trend impact**: low feature-local increase only
|
||||
- **Escalation needed**: none
|
||||
- **Active feature PR close-out entry**: Smoke Coverage
|
||||
- **Planned validation commands**:
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.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/ReviewPack/ReviewPackDownloadTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php`
|
||||
|
||||
## Scope Boundaries
|
||||
|
||||
### In Scope
|
||||
- one on-demand management-ready governance package for one released review context
|
||||
- executive summary in customer-safe language grounded in released review truth
|
||||
- top findings, accepted risks, current governance decisions requiring awareness, and evidence links
|
||||
- reuse of the shared customer-safe interpretation layer from Spec 259, which the package must not bypass
|
||||
- package availability plus explicit `available`, `partial`, `unavailable`, `expired`, and `blocked` states, with stale or entitlement-restricted conditions expressed through those state reasons
|
||||
- explicit current review-pack framing that does not introduce a second downloadable artifact family
|
||||
- explicit auditability for package access or download and secondary evidence or proof opens
|
||||
- localization-ready customer-safe and management-ready wording
|
||||
- reuse of current review-pack, stored-report, evidence-snapshot, and review surfaces rather than a parallel package domain
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- a new panel, portal, customer shell, or separate identity plane
|
||||
- a new report engine, campaign system, schedule, batch flow, newsletter flow, or communications automation
|
||||
- PSA or CRM workflow, service-desk handoff, or broader customer-ops orchestration
|
||||
- review authoring, publishing, regeneration, or evidence or report refresh generation
|
||||
- raw operator-data dumps as the default deliverable
|
||||
- AI-generated summaries or packaging
|
||||
- branding or profile variants beyond neutral v1 governance-package framing over the current export review pack
|
||||
- multi-review or multi-tenant package automation
|
||||
- a new governance-decision workflow state or broader decision board beyond repo-real exception and risk-acceptance truth
|
||||
- a new standalone governance-package persistence family
|
||||
|
||||
## Dependencies
|
||||
|
||||
- current customer review productization contract from `specs/258-customer-review-productization/spec.md`
|
||||
- shared customer-safe interpretation layer from `specs/259-compliance-evidence-mapping/spec.md`
|
||||
- existing `TenantReview`, `ReviewPack`, `EvidenceSnapshot`, `StoredReport`, `Finding`, `FindingException`, and `FindingExceptionDecision` truth
|
||||
- existing review-pack access or download surfaces and evidence or report drilldown surfaces
|
||||
- existing localization, entitlements, capability-first RBAC, and audit foundations
|
||||
|
||||
## Assumptions
|
||||
|
||||
- released review truth remains the authoritative context for stakeholder delivery
|
||||
- the shared customer-safe interpretation layer from Spec 259 exists and is reused; packaging does not compute management meaning separately
|
||||
- an eligible existing review, evidence, and report basis is available for at least one released review context; when it is not, v1 can show explicit unavailable or partial state instead of generating missing sources
|
||||
- current Filament v5 plus Livewire v4 admin-plane surfaces remain the canonical entry points; no panel or provider registration change is required
|
||||
- existing asset strategy is sufficient; this slice does not justify heavy new asset registration
|
||||
- existing review-pack or download entitlement rules remain the governing commercial gate for artifact access
|
||||
- branding or profile variants stay deferred until the repo has a clear existing source of presentation truth that does not add new persistence or configuration scope
|
||||
|
||||
## Risks
|
||||
|
||||
- packaging copy could drift from the shared interpretation layer if implementation reads raw findings or stored reports directly
|
||||
- scope pressure could try to add client-specific branding, profile variants, or bespoke layouts even though v1 intentionally keeps neutral governance-package framing over the current export review pack only
|
||||
- some released reviews may have incomplete evidence or report basis, forcing explicit partial package states
|
||||
- the roadmap phrase `open decisions` could be over-read as a broader queue or approval engine unless the slice stays narrowed to repo-real exception or risk-acceptance decision truth
|
||||
- if package access attempts to trigger missing review-pack or report generation, scope could spill into report-engine or `OperationRun` work
|
||||
|
||||
## Candidate Selection Rationale
|
||||
|
||||
- **Selected candidate**: Governance-as-a-Service Packaging v1
|
||||
- **Source locations**:
|
||||
- `docs/product/spec-candidates.md` active P2 candidate
|
||||
- `docs/product/roadmap.md` priority order item 5 plus the dedicated packaging section
|
||||
- `docs/product/implementation-ledger.md` open gap `Review truth is not yet packaged as a repeatable MSP deliverable`
|
||||
- `specs/258-customer-review-productization/spec.md`
|
||||
- `specs/259-compliance-evidence-mapping/spec.md`
|
||||
- **Why selected**: This is the active unspecced P2 candidate with no direct existing spec. Specs 258 and 259 explicitly defer it as the next bounded follow-up, and the roadmap order plus implementation ledger both place it in the current sellability lane.
|
||||
- **Why this is the smallest viable implementation slice**: It keeps the work on one on-demand management-ready package for one already released review context, using current review, evidence, report, and export artifacts plus shared customer-safe interpretation, with no new package domain, campaign engine, or scheduling system.
|
||||
- **Intentional narrowing from source candidate**: The roadmap and candidate phrase `open decisions` is narrowed here to repo-real governance decision truth from exceptions and risk acceptance, especially current accepted-risk governance decisions and exception decisions that still need follow-up or contextual explanation. V1 does not invent a broader decision queue, approval workflow, or new operator inbox semantics, and it defers bounded MSP branding until the repo has a clear existing source of presentation truth to reuse.
|
||||
- **Why close alternatives are deferred**:
|
||||
- Cross-Tenant Compare and Promotion v1 already has refreshed Spec 043 and remains a separate portfolio-action track.
|
||||
- Workspace, Tenant & Managed Object Lifecycle Governance v1 is explicitly strategic and not ready in the active candidate queue.
|
||||
- External Support Desk / PSA Handoff already has Spec 256 and is a separate commercialization lane.
|
||||
- Customer Review Productization and Compliance Evidence Mapping already have Specs 258 and 259; this package depends on them instead of reopening them.
|
||||
|
||||
## Follow-up Candidates
|
||||
|
||||
- recurring schedule, batch, or campaign packaging only after one on-demand package proves product fit and stays audit-safe
|
||||
- broader branding or profile variants only after the default package remains truthful across customers and the repo has a clear existing presentation-truth source to reuse
|
||||
- portfolio or multi-tenant package automation after compare or promotion and governance decision convergence are materially closed
|
||||
- external delivery workflows such as PSA, service-desk, or communications automation as separate follow-ups
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Produce one repeatable stakeholder package from a released review (Priority: P1)
|
||||
|
||||
An MSP operator wants one package from a released review so they can deliver a recurring governance update without manual slide decks, spreadsheet extracts, or ad-hoc screenshots.
|
||||
|
||||
**Why this priority**: This is the core sellability gap. If the operator still has to assemble the management story outside the product, the feature fails.
|
||||
|
||||
**Independent Test**: Open an eligible released review and access the package that is built from existing review, evidence, report, and review-pack basis without triggering a new reporting or generation workflow.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an entitled actor has a released review with an eligible existing evidence and report basis, **When** they open the released review detail, **Then** they can access one management-ready governance package without starting new review, evidence, or report generation.
|
||||
2. **Given** no eligible released review basis exists, **When** the actor requests the package, **Then** the surface shows an explicit unavailable or partial state instead of a hidden admin path or a new generation workflow.
|
||||
3. **Given** the actor opens the workspace with multiple entitled tenants, **When** they scan the review list, **Then** they can identify which released review context is package-ready before opening detail.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Read management-ready governance meaning without raw operator translation (Priority: P1)
|
||||
|
||||
A customer admin or auditor wants the package to explain what matters now in calm management-ready terms so they can understand the governance position without reading raw operator detail.
|
||||
|
||||
**Why this priority**: Packaging is not useful if it still requires manual translation of technical findings and evidence into stakeholder language.
|
||||
|
||||
**Independent Test**: Open the package or its access context for one released review and confirm that the output summarizes executive position, top findings, accepted risks, governance decisions, and evidence links using shared customer-safe interpretation.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the package is available, **When** the actor opens it or its access context, **Then** the output summarizes executive position, top findings, accepted risks, current governance decisions requiring awareness, and evidence links in customer-safe language.
|
||||
2. **Given** the shared interpretation layer marks a control, readiness summary, or package section as partial, **When** the package is shown, **Then** the package keeps that limitation visible and does not infer stronger management claims from raw evidence.
|
||||
3. **Given** the actor stays in the default package view, **When** deeper stored reports or raw evidence exist, **Then** raw payloads and operator or support diagnostics remain hidden until explicit entitled drilldown.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Trust packaging boundaries, auditability, and scope isolation (Priority: P2)
|
||||
|
||||
An MSP operator wants the package to stay auditable and safe even when secondary access paths are restricted, so stakeholder delivery does not weaken trust or isolation.
|
||||
|
||||
**Why this priority**: A management package only helps if it remains attributable, bounded, and safe to expose across tenant and workspace boundaries.
|
||||
|
||||
**Independent Test**: Access or download the package for an entitled review, then verify auditability, explicit secondary-access unavailability, current review-pack delivery framing, and deny-as-not-found behavior for out-of-scope targets.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the package is accessed or downloaded, **When** the action completes, **Then** the audit trail records that access using the current audit infrastructure.
|
||||
2. **Given** the actor lacks a secondary review-pack or proof-access entitlement inside an otherwise visible review context, **When** the package renders, **Then** the management summary remains readable while secondary artifact access is explicitly unavailable.
|
||||
3. **Given** the actor targets a tenant or review outside their scope, **When** the package or supporting link is opened, **Then** the system resolves as not found and reveals no package, review, or evidence presence.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- a released review exists but the shared interpretation layer or version is unavailable for one section, so the package must show explicit partial or unavailable content and no raw-report fallback
|
||||
- accepted risk exists but current governance is expired, revoked, or rejected, so the package must show governance follow-up needed rather than treat the issue as safely accepted
|
||||
- a review pack exists but a supporting stored report or evidence link has expired or been redacted, so the package must keep the summary truthful and mark the supporting link unavailable
|
||||
- a workspace filter or direct link targets an inaccessible tenant or unreleased review, so the request must resolve safely without leakage
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** This feature introduces no new Microsoft Graph calls, no tenant-changing write path, no new scheduled work, and no new `OperationRun`. It packages already released review truth and already available evidence or report inputs for stakeholder delivery.
|
||||
|
||||
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This follow-up must stay derived. It must not introduce a standalone package source of truth, a new report engine, a campaign system, or a new package-specific taxonomy.
|
||||
|
||||
**Constitution alignment (XCUT-001):** The feature must extend existing review, interpretation, review-pack, evidence, report, localization, and audit paths rather than invent a package-local semantic layer.
|
||||
|
||||
**Constitution alignment (DECIDE-AUD-001 / OPSURF-001):** Default-visible content must remain customer-safe and management-ready, with raw or support detail hidden by default and one dominant next action preserved per surface.
|
||||
|
||||
**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 artifact access remains capability-gated inside an established scope. No new role strings or raw capability strings are introduced by this spec.
|
||||
|
||||
**Constitution alignment (UI-FIL-001 / UI-NAMING-001 / DECIDE-001 / ACTSURF-001):** The slice must remain a native Filament read-only review and export flow with one dominant inspect action on the workspace and one dominant package action on released review detail. No destructive actions are introduced.
|
||||
|
||||
### 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 management-ready package summary from existing released review, review-pack, evidence-snapshot, stored-report, finding, accepted-risk, and exception-decision truth rather than from a new standalone package record.
|
||||
- **FR-003**: The package MUST depend on the shared customer-safe interpretation layer prepared by Spec 259 and MUST NOT render management meaning directly from raw findings, stored reports, or evidence payloads when that layer is absent.
|
||||
- **FR-004**: V1 MUST support exactly one package per released review context at a time and MUST keep packaging anchored to that released review rather than to a broader workspace campaign or portfolio bundle.
|
||||
- **FR-005**: The default-visible package summary MUST answer what review context is covered, what matters now, which findings and accepted risks are most material, which governance decisions require awareness, what evidence basis exists, and what the next stakeholder-facing action is.
|
||||
- **FR-006**: The roadmap and candidate phrase `open decisions` MUST be narrowed in this slice to repo-real governance decision truth rooted in exceptions and risk acceptance. V1 MUST NOT imply a broader approval queue, decision inbox, or new workflow engine.
|
||||
- **FR-007**: Accepted-risk and governance-decision content shown through this slice MUST summarize decision reason, accountable role or person when product truth exists, timing, and expiry, re-review, or follow-up state without exposing internal workflow residue. `accepted_risks` MUST contain currently valid tolerated-risk positions that explain why risk is presently accepted, while `governance_decisions` MUST contain only accepted-risk or exception entries that still require stakeholder awareness or follow-up. The same source decision MUST NOT appear in both lists for the same released review.
|
||||
- **FR-008**: Evidence links in the package MUST point to existing entitled evidence or review-pack surfaces and MUST NOT duplicate raw payloads or provider-debug data as default package content. Existing report detail may only appear when an already-existing entitled viewer seam is available through the current evidence path.
|
||||
- **FR-009**: The package MUST preserve artifact and result truth separation: findings, evidence snapshots, stored reports, review packs, and package summary remain distinct, and the package MUST NOT overwrite or redefine those source records as canonical truth.
|
||||
- **FR-010**: The package MUST remain on-demand and bounded to existing released-review truth. V1 MUST NOT schedule, batch, queue campaigns, or automate multi-pack delivery.
|
||||
- **FR-011**: When required source basis is unavailable, stale, expired, redacted, or incomplete, the surface MUST show explicit `partial`, `unavailable`, `expired`, or `blocked` states with truthful reason messaging rather than generating missing truth or implying false calmness.
|
||||
- **FR-012**: Package availability MUST only be shown for entitled tenants and eligible released reviews in the current workspace.
|
||||
- **FR-013**: Non-members and out-of-scope workspace, tenant, or review requests MUST resolve as deny-as-not-found, while actors inside an established scope MAY receive capability denial on the reused released-review detail route or on gated secondary artifact or proof paths when inherited capability checks deny access.
|
||||
- **FR-014**: Every explicit package access or download and every secondary proof or review-pack action exposed through this slice MUST remain auditable through the current audit infrastructure.
|
||||
- **FR-015**: V1 MUST NOT introduce custom branding, profile variants, or client-specific layout logic. `Download governance package` remains governance-package framing over the current export review pack for the released review and MUST NOT become a second downloadable artifact family.
|
||||
- **FR-016**: Customer-facing and stakeholder-facing labels introduced by this slice MUST remain localization-ready for the existing DE and EN language posture.
|
||||
- **FR-017**: The slice MUST expose no destructive, remediation, publication, regeneration, provider-changing, or admin-only actions in the management-ready package path.
|
||||
- **FR-018**: The slice MUST NOT introduce a new global-searchable resource or widen existing cross-tenant discovery for package, review, report, or evidence artifacts.
|
||||
- **FR-019**: Package summary and package-availability meaning for the same released review MUST stay consistent between workspace summary, released-review detail, and any package-access path.
|
||||
- **FR-020**: The slice MUST reuse current review-pack, evidence, and stored-report artifacts instead of introducing a standalone governance-package report engine or package persistence family.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: V1 MUST introduce no new Graph calls, no new queue or `OperationRun`, and no scheduled or batched runtime path.
|
||||
- **NFR-002**: Asset strategy remains unchanged. If later implementation unexpectedly registers assets, deployment still uses the existing `cd apps/platform && php artisan filament:assets` step.
|
||||
- **NFR-003**: The package MUST remain understandable from the current review surfaces without requiring a second navigation shell or parallel package registry.
|
||||
|
||||
### UX Requirements
|
||||
|
||||
- **UXR-001**: The workspace list keeps one dominant `Open released review` action, and released review detail keeps one dominant package action.
|
||||
- **UXR-002**: Raw or support detail remains secondary and explicit; the workspace list does not gain a peer package row action that competes with review inspection.
|
||||
- **UXR-003**: Package `partial`, `unavailable`, `expired`, and `blocked` states, including stale or entitlement-restricted reasons, are calm, explicit, and management-readable.
|
||||
|
||||
### RBAC / Security Requirements
|
||||
|
||||
- **RBR-001**: The slice MUST reuse the canonical capability registry and MUST NOT introduce raw capability strings or role-name checks in feature code.
|
||||
- **RBR-002**: Package availability, evidence links, report links, and review-pack access MUST NOT leak inaccessible tenant or review hints through counts, labels, empty states, or direct-link responses.
|
||||
|
||||
### Auditability / Observability Requirements
|
||||
|
||||
- **AOR-001**: Package access, package download, and secondary artifact access exposed by this slice MUST be auditable through the current audit infrastructure, with no new parallel audit store.
|
||||
- **AOR-002**: Package output remains attributable to one released review context and one interpretation version so later operators can understand what meaning layer produced the package.
|
||||
|
||||
### Data / Truth-Source Requirements
|
||||
|
||||
- **DTR-001**: `TenantReview`, `ReviewPack`, `EvidenceSnapshot`, `StoredReport`, `Finding`, `FindingException`, `FindingExceptionDecision`, and `AuditLog` remain the authoritative persisted inputs for this slice.
|
||||
- **DTR-002**: Governance package output is a derived packaging layer only and MUST NOT become independent product truth about review state, evidence state, or governance state.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- new package persistence or campaign state
|
||||
- scheduled or batched governance delivery
|
||||
- PSA, CRM, or customer-communications automation
|
||||
- AI-generated package content
|
||||
- review or report generation, publication, or regeneration flows
|
||||
|
||||
## 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 package-generation CTA | `N/A` | `N/A` | yes | Package readiness remains informational on the list surface so the dominant action stays choosing the released review context |
|
||||
| Released Customer Review detail | existing tenant-scoped released review detail surface | `Download governance package` only when the released review and source basis are eligible | `N/A` | `N/A` | none | `N/A` | no additional peer header action beyond the dominant package action; evidence and existing review-pack links stay secondary | `N/A` | yes | Package access stays bound to the current released review and reuses the current export review pack, existing proof, entitlements, and audit gates |
|
||||
|
||||
Action Surface Contract is satisfied for this slice. Each affected surface keeps one dominant inspect or action 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 review surfaces, using explicit empty states, and keeping management-ready emphasis aligned to shared review and interpretation semantics rather than page-local visual language.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Released Review Context**: The existing tenant review chosen for stakeholder delivery and used to anchor all package content.
|
||||
- **Governance Package Output**: The derived management-ready package rendered from existing review, evidence, report, and export truth for one released review. It is not a standalone persisted domain.
|
||||
- **Customer-safe Interpretation Summary**: The shared interpretation output from Spec 259 that converts technical governance truth into management-ready meaning.
|
||||
- **ReviewPack**: The existing packaged export artifact reused as a supporting source or linked artifact for stakeholder delivery.
|
||||
- **EvidenceSnapshot**: The existing proof artifact referenced by package evidence basis and deeper drilldown.
|
||||
- **StoredReport**: The existing report artifact family that may provide supporting source detail but is not the customer-safe package itself.
|
||||
- **Governance Decision Record**: The existing exception or risk-acceptance decision truth, including `FindingExceptionDecision` and current governance validity.
|
||||
- **AuditLog**: The existing audit trail that keeps package and supporting artifact access attributable without introducing a new audit store.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- an authorized actor can access one repeatable on-demand governance package for an eligible released review from the current review surfaces
|
||||
- the package is management-readable and customer-safe by default and clearly represents top findings, accepted risks, current governance decisions, and evidence links
|
||||
- the package depends on the shared customer-safe interpretation layer and never bypasses it with raw provider or report language
|
||||
- the `Download governance package` path remains governance-package framing over the current export review pack and does not introduce a second artifact family
|
||||
- no new package domain, report engine, schedule, or cross-tenant discovery surface is introduced
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: An entitled actor can reach the governance package for an eligible released review in three interactions or fewer from the current customer review workspace.
|
||||
- **SC-002**: In 100% of validated scenarios, the same released review shows consistent executive summary meaning, top findings, accepted-risk governance decisions, and interpretation-dependent messaging across workspace summary, released-review detail, and package access.
|
||||
- **SC-003**: In 100% of validated unauthorized or out-of-scope scenarios, the feature reveals no cross-tenant package, review, report, or evidence presence.
|
||||
- **SC-004**: In 100% of validated incomplete-source scenarios, the package shows an explicit partial or unavailable state instead of implying a complete management package.
|
||||
- **SC-005**: In 100% of validated package-download scenarios, `Download governance package` resolves to the current export review pack for the released review and preserves the ordering and wording of core governance truth and evidence links.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- none
|
||||
210
specs/260-governance-service-packaging/tasks.md
Normal file
210
specs/260-governance-service-packaging/tasks.md
Normal file
@ -0,0 +1,210 @@
|
||||
---
|
||||
description: "Task list for Governance-as-a-Service Packaging v1"
|
||||
---
|
||||
|
||||
# Tasks: Governance-as-a-Service Packaging v1
|
||||
|
||||
**Input**: Design documents from `specs/260-governance-service-packaging/`
|
||||
**Prerequisites**: `specs/260-governance-service-packaging/plan.md` (required), `specs/260-governance-service-packaging/spec.md` (required), `specs/260-governance-service-packaging/research.md`, `specs/260-governance-service-packaging/data-model.md`, `specs/260-governance-service-packaging/quickstart.md`, `specs/260-governance-service-packaging/contracts/governance-service-packaging.openapi.yaml`
|
||||
|
||||
**Tests**: REQUIRED (Pest). Keep proof in the existing `Unit`, `Feature`, and one bounded `Browser` smoke family only; do not add a new heavy-governance, report-engine, or package-specific browser suite for this read-only packaging slice.
|
||||
**Operations**: No new `OperationRun`, queue start, retry flow, report-generation run, scheduling, batching, or campaign orchestration is introduced. The package path must reuse existing released-review truth and current signed review-pack download behavior only.
|
||||
**RBAC**: Workspace membership remains the first `404` boundary; tenant or review scope mismatches remain `404`; the reused released-review detail route and in-scope secondary artifact access may still return inherited `403` capability denials. Reuse `apps/platform/app/Support/Auth/Capabilities.php` and `apps/platform/app/Services/Auth/RoleCapabilityMap.php`; do not introduce raw capability strings or role-name checks.
|
||||
**Shared Pattern Reuse**: Reuse `CustomerReviewWorkspace`, `TenantReviewResource`, `ViewTenantReview`, `TenantReviewComposer`, `TenantReviewSectionFactory`, `ComplianceEvidenceMappingV1`, `ReviewPackService`, `ReviewPackDownloadController`, `ArtifactTruthPresenter`, the current evidence-resource seams, localization files, and the shared audit path. No new `GovernancePackage` domain, report engine, stored-report viewer shell, AI summary layer, or parallel package workflow is allowed.
|
||||
**Filament / Panel Guardrails**: Filament remains v5 on Livewire v4. Provider registration remains unchanged in `apps/platform/bootstrap/providers.php`. `apps/platform/app/Filament/Resources/TenantReviewResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` remain globally disabled. No new panel, provider, destructive package action, or asset strategy change is allowed.
|
||||
**Organization**: Tasks are grouped by user story so package entry, management-ready meaning, and audit or isolation behavior remain independently testable while the implementation stays bounded to one on-demand package for one released review context.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment stays `confidence` plus the existing bounded `browser` smoke and remains the narrowest sufficient proof for this slice.
|
||||
- [x] New or changed tests stay in existing `apps/platform/tests/Unit/TenantReview/`, `apps/platform/tests/Feature/Reviews/`, `apps/platform/tests/Feature/TenantReview/`, `apps/platform/tests/Feature/ReviewPack/`, `apps/platform/tests/Feature/Evidence/`, and `apps/platform/tests/Browser/Reviews/` families only.
|
||||
- [x] Shared helpers, fixtures, membership setup, released-review fixtures, review-pack fixtures, evidence snapshots, and decision fixtures stay cheap by default; no queue or provider-sync setup is added.
|
||||
- [x] Planned validation commands cover package derivation, package access, auditability, and rendered disclosure without widening into unrelated lanes.
|
||||
- [x] The declared surface test profile stays `shared-detail-family` for the released-review detail and `standard-native-filament` for the workspace page.
|
||||
- [x] Browser work stays bounded to the existing `apps/platform/tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` family only.
|
||||
- [x] Any drift toward new package persistence, report engines, scheduling, campaigns, stored-report viewer productization, or broader governance-inbox semantics resolves as `reject-or-split` or `follow-up-spec`, not hidden implementation growth.
|
||||
|
||||
## Phase 1: Setup (Shared Context)
|
||||
|
||||
**Purpose**: Confirm the current review, package, interpretation, localization, audit, and authorization seams before implementation begins.
|
||||
|
||||
- [x] T001 Review `specs/260-governance-service-packaging/spec.md`, `specs/260-governance-service-packaging/plan.md`, `specs/260-governance-service-packaging/research.md`, `specs/260-governance-service-packaging/data-model.md`, `specs/260-governance-service-packaging/quickstart.md`, `specs/260-governance-service-packaging/contracts/governance-service-packaging.openapi.yaml`, `specs/258-customer-review-productization/spec.md`, and `specs/259-compliance-evidence-mapping/spec.md` together so the implementation stays anchored to released-review truth and shared interpretation.
|
||||
- [x] T002 [P] Confirm the workspace and released-review seams in `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`, `apps/platform/app/Filament/Resources/TenantReviewResource.php`, and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`.
|
||||
- [x] T003 [P] Confirm the existing package-delivery and artifact-truth seams in `apps/platform/app/Services/ReviewPackService.php`, `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`, `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, and `apps/platform/app/Services/TenantReviews/TenantReviewComposer.php`.
|
||||
- [x] T004 [P] Confirm the localization, audit, and capability seams in `apps/platform/lang/en/localization.php`, `apps/platform/lang/de/localization.php`, `apps/platform/app/Services/Audit/WorkspaceAuditLogger.php`, `apps/platform/app/Support/Audit/AuditActionId.php`, `apps/platform/app/Support/Auth/Capabilities.php`, and `apps/platform/app/Services/Auth/RoleCapabilityMap.php`.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Foundational (Blocking Prerequisites)
|
||||
|
||||
**Purpose**: Lock the shared package derivation, signed-download reuse, copy, and drift-stop rules that every user story depends on.
|
||||
|
||||
**Critical**: No user-story work should begin until this phase is complete.
|
||||
|
||||
- [x] T005 [P] Extend `apps/platform/tests/Unit/TenantReview/TenantReviewComposerTest.php` and `apps/platform/tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php` to lock package derivation to existing review summary and section truth, without a new `GovernancePackage` model, persistence table, or report namespace.
|
||||
- [x] T006 [P] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php` and `apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php` to prove the customer-workspace package path reuses `downloadCurrentReviewPackAction()` and the current signed review-pack download behavior rather than export generation, `ReviewPackService::generateFromReview()`, or any `OperationRun` start path.
|
||||
- [x] T007 Implement the derived package-summary and package-availability seam inside `apps/platform/app/Services/TenantReviews/TenantReviewComposer.php`, `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php`, and `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php` without introducing a new packaging service layer, persisted lifecycle, or broader decision framework.
|
||||
- [x] T008 [P] Add shared management-ready vocabulary and calm unavailable-state copy in `apps/platform/lang/en/localization.php` and `apps/platform/lang/de/localization.php`, keeping the roadmap phrase `open decisions` narrowed to accepted-risk and exception-decision truth only.
|
||||
- [x] T009 [P] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php` and `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` to block drift into a competing workspace package action, operator-only export actions in customer-workspace mode, or a new stored-report viewer shell from the package path.
|
||||
|
||||
**Checkpoint**: Package derivation, signed-download reuse, localized vocabulary, and the main drift-stop guards are in place before surface-specific work starts.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 1 - Produce One Repeatable Stakeholder Package From A Released Review (Priority: P1)
|
||||
|
||||
**Goal**: Give an entitled actor one on-demand package entry for one released review context without starting a new reporting or generation workflow.
|
||||
|
||||
**Independent Test**: Open an eligible released review from the current workspace, confirm the workspace shows package readiness as information only, and confirm the released-review detail reuses the current signed review-pack delivery seam without triggering generation.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [x] T010 [P] [US1] Extend `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php` to cover package readiness, management teaser copy, evidence-basis disclosure, calm `partial`, `unavailable`, `expired`, and `blocked` states, and the absence of a competing package row action.
|
||||
- [x] T011 [P] [US1] Extend `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` and `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` to cover released-review launch, package-ready signaling, explicit unavailable-state reasons, governance-package framing over the current export review pack, and signed review-pack reuse from the workspace flow.
|
||||
- [x] T012 [P] [US1] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php` and `apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php` to assert one dominant `Download governance package` action in customer-workspace mode, explicit framing over the current export review pack, and no export-generation fallback when source basis is missing.
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [x] T013 [US1] Update `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` and `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php` to show package readiness, management teaser, evidence basis, and calm unavailable messaging while keeping `Open released review` as the only dominant action.
|
||||
- [x] T014 [US1] Update `apps/platform/app/Filament/Resources/TenantReviewResource.php` and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` so the released-review detail owns the package context in `customer_workspace=1` mode and reuses `downloadCurrentReviewPackAction()` instead of `exportExecutivePackAction()` or any generation path.
|
||||
- [x] T015 [US1] Update `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` and `apps/platform/app/Services/ReviewPackService.php` only as needed to carry source-surface, review, tenant-filter, and interpretation metadata through the existing signed download seam without creating a new artifact namespace or package-generation entry point.
|
||||
Repo truth: the existing signed download seam already carried `source_surface`, `review_id`, `tenant_filter_id`, and `interpretation_version`, so no controller or service code change was required.
|
||||
|
||||
**Checkpoint**: User Story 1 is independently functional when one released review context exposes a repeatable package path that reuses current review-pack truth instead of starting export generation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 2 - Read Management-Ready Governance Meaning Without Raw Operator Translation (Priority: P1)
|
||||
|
||||
**Goal**: Keep the package management-readable and customer-safe by deriving its meaning from shared interpretation and existing review sections only.
|
||||
|
||||
**Independent Test**: Open one released-review package context and verify that executive summary, top findings, accepted risks, governance decisions, evidence basis, and next action all come from the shared interpretation layer and current review truth, with raw/support detail hidden by default.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [x] T016 [P] [US2] Extend `apps/platform/tests/Unit/TenantReview/TenantReviewComposerTest.php` to cover management-ready executive summary, top findings, evidence-basis summary, supporting artifact links, and explicit partial states when interpretation truth is incomplete.
|
||||
- [x] T017 [P] [US2] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php` to cover customer-safe management summary content, hidden raw or support detail by default, and package-summary consistency between released-review detail and supporting artifacts.
|
||||
- [x] T018 [P] [US2] Extend `apps/platform/tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php` and `apps/platform/tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.php` to keep accepted-risk and exception follow-up language narrowed to current governance-decision truth, assert `accepted_risks` and `governance_decisions` stay disjoint, and prevent drift into a broader decision inbox or workflow board.
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [x] T019 [US2] Update `apps/platform/app/Services/TenantReviews/TenantReviewComposer.php` and `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php` to derive package summary fields from existing `control_interpretation`, `highlights`, `accepted_risks`, and related review section payloads without a new package-local mapper, report engine, or AI summary layer, while keeping stable accepted-risk entries separate from governance-decision follow-up entries.
|
||||
- [x] T020 [US2] Update `apps/platform/app/Support/Governance/Controls/ComplianceEvidenceMappingV1.php` and `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php` to keep calm disclosure, package availability messaging, and supporting evidence or review-pack links aligned with shared interpretation and artifact truth, using the canonical mapping `publishable -> available`, `internal_only/stale -> partial`, `blocked/missing_input -> unavailable`, `historical_only -> expired`, and package-level entitlement restrictions -> `blocked`, while keeping secondary-proof restrictions on supporting-link disclosure instead of blocking the package summary.
|
||||
- [x] T021 [US2] Finalize localized DE and EN management-ready copy in `apps/platform/lang/en/localization.php` and `apps/platform/lang/de/localization.php` for executive summary labels, evidence-basis messaging, accepted-risk follow-up, governance-decision wording, and explicit `partial`, `unavailable`, `expired`, and `blocked` states with truthful stale or entitlement-restricted reasons.
|
||||
|
||||
**Checkpoint**: User Story 2 is independently functional when the same released review shows consistent customer-safe package meaning across workspace summary, released-review detail, and package access without raw operator translation.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 3 - Trust Packaging Boundaries, Auditability, And Scope Isolation (Priority: P2)
|
||||
|
||||
**Goal**: Keep package delivery attributable, tenant-safe, and bounded even when secondary proof access is limited.
|
||||
|
||||
**Independent Test**: Access or download the package for an entitled review, verify current audit coverage, verify explicit secondary-artifact unavailability where needed, and confirm out-of-scope tenant or review targets resolve without leakage.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [x] T022 [P] [US3] Extend `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php` and `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php` to cover deny-as-not-found package readiness, tenant-prefilter isolation, and no review, package, report, or evidence hint leakage across tenants.
|
||||
- [x] T023 [P] [US3] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php`, `apps/platform/tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php`, and `apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php` so the reused released-review detail route can assert inherited in-scope `403` capability denial, while secondary review-pack and evidence links stay capability-gated and the management summary remains readable when only supporting access is restricted.
|
||||
- [x] T024 [P] [US3] Extend `apps/platform/tests/Feature/TenantReview/TenantReviewAuditLogTest.php` and `apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php` to assert package access, signed download, and proof opens stay on the current audit infrastructure with source-surface and interpretation-version metadata.
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [x] T025 [US3] Update `apps/platform/app/Services/Audit/WorkspaceAuditLogger.php`, `apps/platform/app/Support/Audit/AuditActionId.php`, `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` to reuse current audit events and metadata instead of creating a new package audit family.
|
||||
Repo truth: the existing audit action IDs and logger already covered workspace open, tenant-review open, signed pack download, and evidence open with the required metadata, so no new audit family was added.
|
||||
- [x] T026 [US3] Keep neutral governance-package framing and secondary artifact disclosure inside `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `apps/platform/resources/views/filament/pages/reviews/customer-review-workspace.blade.php`, and existing evidence or review-pack link surfaces so `Download governance package` always maps to the current export review pack and no branding or profile-variant system is introduced in v1.
|
||||
- [x] T027 [US3] Handle missing, expired, redacted, or unavailable supporting artifacts in `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` so the slice stays explicit about unavailable proof and does not add a stored-report viewer shell, AI summary fallback, schedule, batch, campaign, or broader governance-inbox semantics.
|
||||
|
||||
**Checkpoint**: User Story 3 is independently functional when package access remains attributable, bounded, and tenant-safe without widening the slice into new workflows or surfaces.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Validation
|
||||
|
||||
**Purpose**: Finish focused validation, formatting, and explicit drift checks without widening scope.
|
||||
|
||||
- [x] T028 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/TenantReview/TenantReviewComposerTest.php tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`.
|
||||
- [x] T029 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php tests/Feature/Reviews/CustomerReviewWorkspaceNavigationContextTest.php tests/Feature/TenantReview/TenantReviewExplanationSurfaceTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/TenantReview/TenantReviewAuditLogTest.php tests/Feature/TenantReview/TenantReviewExecutivePackTest.php tests/Feature/ReviewPack/TenantReviewDerivedReviewPackTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php tests/Feature/ReviewPack/ReviewPackValidRiskAcceptanceTest.php tests/Feature/Evidence/ExceptionValidityEvidenceIntegrationTest.php tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`.
|
||||
- [x] T030 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Reviews/CustomerReviewWorkspaceSmokeTest.php` and keep browser work bounded to this existing smoke family only.
|
||||
- [x] T031 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` for all touched platform files.
|
||||
- [x] T032 [P] Review touched code in `apps/platform/app/Services/ReviewPackService.php`, `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`, `apps/platform/app/Services/TenantReviews/TenantReviewComposer.php`, and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` to confirm the package path reuses current review-pack truth and signed download behavior rather than new persistence, export generation, a report engine, an `OperationRun`, or schedule, batch, or campaign workflows.
|
||||
- [x] T033 [P] Review touched code in `apps/platform/bootstrap/providers.php`, `apps/platform/app/Filament/Resources/TenantReviewResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, touched Blade views, and touched localization files to confirm no new panel/provider, global-search surface, asset strategy, stored-report viewer shell, AI summaries, or broader governance-inbox semantics were introduced.
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
### Phase Dependencies
|
||||
|
||||
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user stories.
|
||||
- **Phase 3 (US1)**: depends on Phase 2 and establishes the one released-review package entry path.
|
||||
- **Phase 4 (US2)**: depends on Phase 2 and should ship with US1 so the package entry path and its management-ready meaning stay aligned.
|
||||
- **Phase 5 (US3)**: depends on Phase 2 and is safest after US1 and US2 because audit and isolation checks rely on the package surfaces already existing.
|
||||
- **Phase 6 (Polish)**: depends on all desired user stories being complete.
|
||||
|
||||
### User Story Dependencies
|
||||
|
||||
- **US1 (P1)**: independently testable after Phase 2 and delivers the core on-demand package path for one released review.
|
||||
- **US2 (P1)**: independently testable after Phase 2 and should land with US1 so the package does not expose entry without trustworthy management meaning.
|
||||
- **US3 (P2)**: independently testable after Phase 2 and hardens auditability, secondary-access handling, and scope isolation after the core path exists.
|
||||
|
||||
### Within Each User Story
|
||||
|
||||
- Write the listed Pest coverage first for each story and make it fail for the intended gap.
|
||||
- Keep implementation inside the existing review, review-pack, interpretation, evidence, localization, and audit seams named in the plan.
|
||||
- Re-run the narrowest relevant validation command after each story checkpoint before moving to the next story.
|
||||
|
||||
---
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
### Setup / Foundations
|
||||
|
||||
- T002, T003, and T004 can run in parallel during seam confirmation.
|
||||
- T005, T006, T008, and T009 can run in parallel before the shared package seam in T007 is finalized.
|
||||
|
||||
### User Story 1
|
||||
|
||||
- T010, T011, and T012 can run in parallel before runtime edits begin.
|
||||
- After the package-entry contract settles, T013 and T015 can proceed in parallel while T014 finalizes the released-review detail surface.
|
||||
|
||||
### User Story 2
|
||||
|
||||
- T016, T017, and T018 can run in parallel because they cover different aspects of the meaning contract.
|
||||
- T019 and T020 can proceed together once the failing tests prove the gap; T021 can land in parallel with the final copy pass.
|
||||
|
||||
### User Story 3
|
||||
|
||||
- T022, T023, and T024 can run in parallel because they cover authorization, secondary-access, and audit behavior separately.
|
||||
- T025 and T026 can proceed together once the package path is stable; T027 should follow to finish the explicit unavailable-state handling.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Suggested MVP Scope
|
||||
|
||||
- MVP = **US1 + US2 together**. The feature is only product-meaningful when the actor can both reach the package from one released review and trust the management-ready meaning shown there.
|
||||
|
||||
### Incremental Delivery
|
||||
|
||||
1. Complete Phase 1 and Phase 2.
|
||||
2. Deliver US1 and US2 together as the smallest sellable packaging slice.
|
||||
3. Add US3 to harden auditability, secondary-access boundaries, and explicit unavailable behavior.
|
||||
4. Finish with the focused validation and drift-review tasks in Phase 6.
|
||||
|
||||
### Team Strategy
|
||||
|
||||
1. Settle the shared package-derivation and signed-download guardrails first.
|
||||
2. Parallelize focused tests inside each story before widening implementation.
|
||||
3. Serialize merges around `CustomerReviewWorkspace`, `ViewTenantReview`, and the shared localization files so the package vocabulary and action hierarchy stay coherent.
|
||||
|
||||
---
|
||||
|
||||
## Deferred Follow-Ups / Non-Goals
|
||||
|
||||
- Multi-review or multi-tenant package automation stays deferred.
|
||||
- Stored-report viewer productization stays deferred unless a repo-real existing viewer seam proves insufficient.
|
||||
- Schedule, batch, campaign, PSA, CRM, and customer-communications workflows stay deferred.
|
||||
- Package profile variants or broader branding systems stay deferred beyond neutral v1 governance-package framing over the current export review pack.
|
||||
Loading…
Reference in New Issue
Block a user