feat: add governance artifact lifecycle retention contracts (#477)
Automated PR provided by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #477
This commit is contained in:
parent
686947d26c
commit
bd6f59bb7c
@ -1953,15 +1953,7 @@ private function reviewPackDownloadUrl(EnvironmentReview $review, ManagedEnviron
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! filled($pack->file_path) || ! filled($pack->file_disk)) {
|
||||
if (! app(ReviewPackService::class)->hasDownloadableArtifact($pack)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -2182,7 +2174,7 @@ private function governancePackageAvailability(ManagedEnvironment $tenant): arra
|
||||
];
|
||||
}
|
||||
|
||||
if (! filled($pack->file_path) || ! filled($pack->file_disk)) {
|
||||
if (! app(ReviewPackService::class)->hasDownloadableArtifact($pack)) {
|
||||
return [
|
||||
'state' => 'not_available',
|
||||
'label' => $this->canonicalCustomerReviewStateLabel('not_available'),
|
||||
@ -2491,15 +2483,7 @@ private function reviewPackHasReadyExport(?ReviewPack $pack): bool
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filled($pack->file_path) && filled($pack->file_disk);
|
||||
return app(ReviewPackService::class)->hasDownloadableArtifact($pack);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -359,9 +359,12 @@ public static function table(Table $table): Table
|
||||
->label(fn (ReviewPack $record): string => static::downloadActionLabelFor($record))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('gray')
|
||||
->visible(fn (ReviewPack $record): bool => $record->status === ReviewPackStatus::Ready->value)
|
||||
->visible(fn (ReviewPack $record): bool => static::canDownloadReviewPack($record))
|
||||
->url(function (ReviewPack $record): string {
|
||||
return app(ReviewPackService::class)->generateDownloadUrl($record);
|
||||
return app(ReviewPackService::class)->generateDownloadUrl(
|
||||
$record,
|
||||
static::reviewPackDownloadParameters($record),
|
||||
);
|
||||
})
|
||||
->openUrlInNewTab(),
|
||||
Actions\ActionGroup::make([
|
||||
@ -374,12 +377,19 @@ public static function table(Table $table): Table
|
||||
->requiresConfirmation()
|
||||
->modalDescription('This will mark the pack as expired and delete the file. This cannot be undone.')
|
||||
->action(function (ReviewPack $record): void {
|
||||
if ($record->file_path && $record->file_disk) {
|
||||
\Illuminate\Support\Facades\Storage::disk($record->file_disk)->delete($record->file_path);
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user instanceof User) {
|
||||
Notification::make()
|
||||
->danger()
|
||||
->title('Unable to expire review pack')
|
||||
->send();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$record->update(['status' => ReviewPackStatus::Expired->value]);
|
||||
static::truthEnvelope($record->refresh(), fresh: true);
|
||||
$expiredPack = app(ReviewPackService::class)->expire($record, $user, 'review_pack_resource');
|
||||
static::truthEnvelope($expiredPack, fresh: true);
|
||||
|
||||
Notification::make()
|
||||
->success()
|
||||
@ -465,17 +475,51 @@ public static function getPages(): array
|
||||
|
||||
public static function downloadActionLabelFor(ReviewPack $record): string
|
||||
{
|
||||
$actor = auth()->user();
|
||||
$decision = app(CustomerOutputGate::class)->decisionForReviewPack(
|
||||
$record,
|
||||
$actor instanceof User ? $actor : null,
|
||||
);
|
||||
$decision = static::customerOutputDecisionFor($record);
|
||||
|
||||
return $decision->canStreamCustomerOutput
|
||||
? __('localization.review.download_customer_safe_review_pack')
|
||||
: __('localization.review.download_internal_preview');
|
||||
}
|
||||
|
||||
public static function canDownloadReviewPack(ReviewPack $record): bool
|
||||
{
|
||||
if (! app(ReviewPackService::class)->hasDownloadableArtifact($record)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$decision = static::customerOutputDecisionFor($record);
|
||||
|
||||
return $decision->canStreamCustomerOutput || $decision->canStreamInternalPreview;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, scalar|null>
|
||||
*/
|
||||
public static function reviewPackDownloadParameters(ReviewPack $record): array
|
||||
{
|
||||
$decision = static::customerOutputDecisionFor($record);
|
||||
|
||||
if ($decision->canStreamCustomerOutput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
CustomerOutputGate::INTERNAL_PREVIEW_QUERY_KEY => 1,
|
||||
'source_surface' => 'review_pack',
|
||||
];
|
||||
}
|
||||
|
||||
private static function customerOutputDecisionFor(ReviewPack $record): \App\Support\ReviewPacks\CustomerOutputGateDecision
|
||||
{
|
||||
$actor = auth()->user();
|
||||
|
||||
return app(CustomerOutputGate::class)->decisionForReviewPack(
|
||||
$record,
|
||||
$actor instanceof User ? $actor : null,
|
||||
);
|
||||
}
|
||||
|
||||
public static function isCustomerWorkspaceFlow(): bool
|
||||
{
|
||||
return request()->query('source_surface') === CustomerReviewWorkspace::SOURCE_SURFACE;
|
||||
|
||||
@ -49,7 +49,8 @@ protected function getHeaderActions(): array
|
||||
->label(fn (): string => ReviewPackResource::downloadActionLabelFor($this->record))
|
||||
->icon('heroicon-o-arrow-down-tray')
|
||||
->color('gray')
|
||||
->visible(fn (): bool => $this->customerOutputDecision()->canStreamCustomerOutput)
|
||||
->visible(fn (): bool => app(ReviewPackService::class)->hasDownloadableArtifact($this->record)
|
||||
&& $this->customerOutputDecision()->canStreamCustomerOutput)
|
||||
->url(fn (): string => app(ReviewPackService::class)->generateDownloadUrl($this->record, [
|
||||
'source_surface' => CustomerReviewWorkspace::SOURCE_SURFACE,
|
||||
]))
|
||||
@ -412,9 +413,7 @@ private function canOpenRenderedReport(bool $requiresCustomerOutput = false): bo
|
||||
|
||||
private function canDownloadReviewPack(): bool
|
||||
{
|
||||
$decision = $this->customerOutputDecision();
|
||||
|
||||
return $decision->canStreamCustomerOutput || $decision->canStreamInternalPreview;
|
||||
return ReviewPackResource::canDownloadReviewPack($this->record);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -422,16 +421,7 @@ private function canDownloadReviewPack(): bool
|
||||
*/
|
||||
private function reviewPackDownloadParameters(): array
|
||||
{
|
||||
$decision = $this->customerOutputDecision();
|
||||
|
||||
if ($decision->canStreamCustomerOutput) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
CustomerOutputGate::INTERNAL_PREVIEW_QUERY_KEY => 1,
|
||||
'source_surface' => 'review_pack',
|
||||
];
|
||||
return ReviewPackResource::reviewPackDownloadParameters($this->record);
|
||||
}
|
||||
|
||||
private function customerOutputDecision(): CustomerOutputGateDecision
|
||||
|
||||
@ -8,12 +8,11 @@
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Services\Audit\WorkspaceAuditLogger;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\ReviewPacks\CustomerOutputGate;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
@ -36,21 +35,9 @@ public function __invoke(Request $request, ReviewPack $reviewPack): StreamedResp
|
||||
abort(403);
|
||||
}
|
||||
|
||||
if ($reviewPack->status !== ReviewPackStatus::Ready->value) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
$artifact = app(ReviewPackService::class)->resolveDownloadableArtifact($reviewPack);
|
||||
|
||||
if ($reviewPack->expires_at && $reviewPack->expires_at->isPast()) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
if (! filled($reviewPack->file_path) || ! filled($reviewPack->file_disk)) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
$disk = Storage::disk($reviewPack->file_disk ?? 'exports');
|
||||
|
||||
if (! $disk->exists($reviewPack->file_path)) {
|
||||
if ($artifact === null) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
@ -93,8 +80,10 @@ public function __invoke(Request $request, ReviewPack $reviewPack): StreamedResp
|
||||
$reviewPack->generated_at?->format('Y-m-d') ?? now()->format('Y-m-d'),
|
||||
);
|
||||
|
||||
return $disk->download($reviewPack->file_path, $filename, [
|
||||
'X-Review-Pack-SHA256' => $reviewPack->sha256 ?? '',
|
||||
return response()->streamDownload(static function () use ($artifact): void {
|
||||
echo $artifact['bytes'];
|
||||
}, $filename, [
|
||||
'X-Review-Pack-SHA256' => $artifact['sha256'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -69,6 +69,10 @@ public function __invoke(Request $request, ReviewPack $reviewPack): Response
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
if (! app(ReviewPackService::class)->hasDownloadableArtifact($reviewPack)) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
$gateDecision = app(CustomerOutputGate::class)->decisionForReviewPack($reviewPack, $user);
|
||||
$profile = $this->profileState($request, $gateDecision->guidanceState);
|
||||
|
||||
@ -527,7 +531,7 @@ private function downloadUrl(
|
||||
CustomerOutputGateDecision $gateDecision,
|
||||
array $profile,
|
||||
): ?string {
|
||||
if (! filled($reviewPack->file_path) || ! filled($reviewPack->file_disk)) {
|
||||
if (! app(ReviewPackService::class)->hasDownloadableArtifact($reviewPack)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@ -18,11 +18,16 @@
|
||||
use App\Services\Evidence\EvidenceResolutionRequest;
|
||||
use App\Services\Evidence\EvidenceSnapshotResolver;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
|
||||
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Throwable;
|
||||
|
||||
class ReviewPackService
|
||||
{
|
||||
@ -257,6 +262,141 @@ public function generateDownloadUrl(ReviewPack $pack, array $parameters = []): s
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{path: string, bytes: string, size: int, sha256: string}|null
|
||||
*/
|
||||
public function resolveDownloadableArtifact(ReviewPack $pack): ?array
|
||||
{
|
||||
if ($pack->status !== ReviewPackStatus::Ready->value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->expires_at !== null && $pack->expires_at->isPast()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($pack->file_disk !== 'exports' || ! filled($pack->file_path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int) $pack->file_size <= 0 || ! filled($pack->sha256)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$path = (string) $pack->file_path;
|
||||
$disk = Storage::disk('exports');
|
||||
|
||||
try {
|
||||
if (! $disk->exists($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$fileSize = (int) $disk->size($path);
|
||||
$fileBytes = $disk->get($path);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! is_string($fileBytes)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($fileSize <= 0 || $fileSize !== (int) $pack->file_size) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! hash_equals((string) $pack->sha256, hash('sha256', $fileBytes))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'path' => $path,
|
||||
'bytes' => $fileBytes,
|
||||
'size' => $fileSize,
|
||||
'sha256' => (string) $pack->sha256,
|
||||
];
|
||||
}
|
||||
|
||||
public function hasDownloadableArtifact(ReviewPack $pack): bool
|
||||
{
|
||||
return $this->resolveDownloadableArtifact($pack) !== null;
|
||||
}
|
||||
|
||||
public function expire(ReviewPack $reviewPack, User $actor, string $sourceSurface = 'review_pack'): ReviewPack
|
||||
{
|
||||
$reviewPack->loadMissing(['tenant.workspace']);
|
||||
|
||||
$tenant = $reviewPack->tenant;
|
||||
|
||||
if (! $tenant instanceof ManagedEnvironment || ! $tenant->workspace) {
|
||||
throw new AuthorizationException('Review pack scope is unavailable.');
|
||||
}
|
||||
|
||||
if (! $actor->canAccessTenant($tenant)) {
|
||||
throw new AuthorizationException('Review pack not found.');
|
||||
}
|
||||
|
||||
Gate::forUser($actor)->authorize(Capabilities::REVIEW_PACK_MANAGE, $tenant);
|
||||
|
||||
$beforeStatus = (string) $reviewPack->status;
|
||||
|
||||
if ($beforeStatus !== ReviewPackStatus::Ready->value) {
|
||||
throw new \InvalidArgumentException('Only ready review packs can be expired.');
|
||||
}
|
||||
|
||||
$fileDeleted = false;
|
||||
$fileWasPresent = false;
|
||||
$fileDisk = is_string($reviewPack->file_disk) ? $reviewPack->file_disk : null;
|
||||
$filePath = is_string($reviewPack->file_path) ? $reviewPack->file_path : null;
|
||||
|
||||
if ($fileDisk === 'exports' && filled($filePath)) {
|
||||
$disk = Storage::disk('exports');
|
||||
|
||||
if ($disk->exists((string) $filePath)) {
|
||||
$fileWasPresent = true;
|
||||
|
||||
if (! $disk->delete((string) $filePath) || $disk->exists((string) $filePath)) {
|
||||
throw new \RuntimeException('Unable to delete the review pack file before expiring it.');
|
||||
}
|
||||
|
||||
$fileDeleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
$reviewPack->forceFill([
|
||||
'status' => ReviewPackStatus::Expired->value,
|
||||
])->save();
|
||||
|
||||
$this->auditLogger->log(
|
||||
workspace: $tenant->workspace,
|
||||
action: AuditActionId::ReviewPackExpired,
|
||||
context: [
|
||||
'metadata' => [
|
||||
'review_pack_id' => (int) $reviewPack->getKey(),
|
||||
'environment_review_id' => $reviewPack->environment_review_id !== null
|
||||
? (int) $reviewPack->environment_review_id
|
||||
: null,
|
||||
'artifact_family' => 'review_pack',
|
||||
'before_status' => $beforeStatus,
|
||||
'after_status' => ReviewPackStatus::Expired->value,
|
||||
'file_disk' => $fileDisk,
|
||||
'file_path_present' => filled($filePath),
|
||||
'file_present_before' => $fileWasPresent,
|
||||
'file_deleted' => $fileDeleted,
|
||||
'source_surface' => $sourceSurface,
|
||||
],
|
||||
],
|
||||
actor: $actor,
|
||||
resourceType: 'review_pack',
|
||||
resourceId: (string) $reviewPack->getKey(),
|
||||
targetLabel: sprintf('Review pack #%d', (int) $reviewPack->getKey()),
|
||||
tenant: $tenant,
|
||||
operationRunId: $reviewPack->operation_run_id,
|
||||
);
|
||||
|
||||
return $reviewPack->refresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a signed rendered-report URL for a review pack.
|
||||
*
|
||||
|
||||
@ -126,6 +126,7 @@ enum AuditActionId: string
|
||||
case ReviewPublicationResolutionSuperseded = 'review_publication_resolution.superseded';
|
||||
case CustomerReviewWorkspaceOpened = 'customer_review_workspace.opened';
|
||||
case ReviewPackDownloaded = 'review_pack.downloaded';
|
||||
case ReviewPackExpired = 'review_pack.expired';
|
||||
case ManagementReportPdfGenerationRequested = 'management_report_pdf.generation_requested';
|
||||
case ManagementReportPdfGenerationBlocked = 'management_report_pdf.generation_blocked';
|
||||
case ManagementReportPdfGenerated = 'management_report_pdf.generated';
|
||||
@ -314,6 +315,7 @@ private static function labels(): array
|
||||
self::ReviewPublicationResolutionSuperseded->value => 'Review publication resolution superseded',
|
||||
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||
self::ReviewPackExpired->value => 'Review pack expired',
|
||||
self::ManagementReportPdfGenerationRequested->value => 'Management report PDF generation requested',
|
||||
self::ManagementReportPdfGenerationBlocked->value => 'Management report PDF generation blocked',
|
||||
self::ManagementReportPdfGenerated->value => 'Management report PDF generated',
|
||||
@ -446,6 +448,7 @@ private static function summaries(): array
|
||||
self::ReviewPublicationResolutionSuperseded->value => 'Review publication resolution superseded',
|
||||
self::CustomerReviewWorkspaceOpened->value => 'Customer review workspace opened',
|
||||
self::ReviewPackDownloaded->value => 'Review pack downloaded',
|
||||
self::ReviewPackExpired->value => 'Review pack expired',
|
||||
self::ManagementReportPdfGenerationRequested->value => 'Management report PDF generation requested',
|
||||
self::ManagementReportPdfGenerationBlocked->value => 'Management report PDF generation blocked',
|
||||
self::ManagementReportPdfGenerated->value => 'Management report PDF generated',
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\User;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\ReviewPackStatus;
|
||||
|
||||
@ -101,11 +102,7 @@ public function decisionForReviewPack(ReviewPack $reviewPack, ?User $actor = nul
|
||||
|
||||
private function hasReadyArtifact(ReviewPack $reviewPack): bool
|
||||
{
|
||||
if ($reviewPack->status !== ReviewPackStatus::Ready->value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return filled($reviewPack->file_path) && filled($reviewPack->file_disk);
|
||||
return app(ReviewPackService::class)->hasDownloadableArtifact($reviewPack);
|
||||
}
|
||||
|
||||
private function isExpired(ReviewPack $reviewPack): bool
|
||||
|
||||
@ -301,6 +301,27 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
||||
->and(OperationRun::query()->count())->toBe($operationRunCount);
|
||||
});
|
||||
|
||||
it('Spec406 refuses rendered report signed URLs when the review pack artifact fails validation', function (): void {
|
||||
[$user, , $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
||||
|
||||
$signedUrl = app(ReviewPackService::class)->generateRenderedReportUrl($pack, [
|
||||
'source_surface' => 'spec406',
|
||||
'review_id' => (int) $review->getKey(),
|
||||
]);
|
||||
|
||||
$pack->forceFill([
|
||||
'sha256' => str_repeat('0', 64),
|
||||
])->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($signedUrl)
|
||||
->assertNotFound();
|
||||
|
||||
expect(AuditLog::query()
|
||||
->where('action', AuditActionId::ReviewPackDownloaded->value)
|
||||
->count())->toBe(0);
|
||||
});
|
||||
|
||||
it('renders limitations near the top without claiming customer-safe readiness', function (): void {
|
||||
[$user, $tenant, $review, $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
||||
restateEnvironmentReviewEvidenceSnapshot($review->evidenceSnapshot, EvidenceCompletenessState::Partial);
|
||||
@ -595,6 +616,90 @@ function suspendReadyPackWorkspaceForDownloadTest(ReviewPack $pack): void
|
||||
$response->assertNotFound();
|
||||
});
|
||||
|
||||
it('Spec406 refuses review pack downloads when file metadata and bytes are inconsistent', function (\Closure $mutatePack): void {
|
||||
[$user, , , $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
||||
$mutatePack($pack);
|
||||
$pack->refresh();
|
||||
|
||||
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'source_surface' => 'spec406',
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($signedUrl)
|
||||
->assertNotFound();
|
||||
|
||||
expect(AuditLog::query()
|
||||
->where('action', AuditActionId::ReviewPackDownloaded->value)
|
||||
->count())->toBe(0);
|
||||
})->with([
|
||||
'zero-byte file' => [
|
||||
function (ReviewPack $pack): void {
|
||||
Storage::disk('exports')->put((string) $pack->file_path, '');
|
||||
$pack->forceFill([
|
||||
'file_size' => 0,
|
||||
'sha256' => hash('sha256', ''),
|
||||
])->save();
|
||||
},
|
||||
],
|
||||
'file-size mismatch' => [
|
||||
function (ReviewPack $pack): void {
|
||||
$bytes = Storage::disk('exports')->get((string) $pack->file_path);
|
||||
$pack->forceFill([
|
||||
'file_size' => strlen($bytes) + 10,
|
||||
])->save();
|
||||
},
|
||||
],
|
||||
'sha mismatch' => [
|
||||
function (ReviewPack $pack): void {
|
||||
$pack->forceFill([
|
||||
'sha256' => str_repeat('0', 64),
|
||||
])->save();
|
||||
},
|
||||
],
|
||||
'missing sha' => [
|
||||
function (ReviewPack $pack): void {
|
||||
$pack->forceFill([
|
||||
'sha256' => null,
|
||||
])->save();
|
||||
},
|
||||
],
|
||||
'wrong disk' => [
|
||||
function (ReviewPack $pack): void {
|
||||
Storage::fake('public');
|
||||
$bytes = Storage::disk('exports')->get((string) $pack->file_path);
|
||||
$publicPath = 'review-packs/spec406-public.zip';
|
||||
Storage::disk('public')->put($publicPath, $bytes);
|
||||
|
||||
$pack->forceFill([
|
||||
'file_disk' => 'public',
|
||||
'file_path' => $publicPath,
|
||||
'file_size' => strlen($bytes),
|
||||
'sha256' => hash('sha256', $bytes),
|
||||
])->save();
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
it('Spec406 rechecks review pack lifecycle after a signed URL was created', function (): void {
|
||||
[$user, , , $pack] = createCurrentReviewPackForRenderedReport(customerSafeReady: true);
|
||||
$signedUrl = app(ReviewPackService::class)->generateDownloadUrl($pack, [
|
||||
'source_surface' => 'spec406',
|
||||
]);
|
||||
|
||||
$pack->forceFill([
|
||||
'status' => ReviewPackStatus::Failed->value,
|
||||
])->save();
|
||||
|
||||
$this->actingAs($user)
|
||||
->get($signedUrl)
|
||||
->assertNotFound();
|
||||
|
||||
expect(AuditLog::query()
|
||||
->where('action', AuditActionId::ReviewPackDownloaded->value)
|
||||
->count())->toBe(0);
|
||||
});
|
||||
|
||||
// ─── Unsigned URL → 403 ─────────────────────────────────────
|
||||
|
||||
it('returns 403 for an unsigned URL', function (): void {
|
||||
|
||||
@ -4,14 +4,17 @@
|
||||
|
||||
use App\Filament\Resources\ReviewPackResource;
|
||||
use App\Filament\Resources\ReviewPackResource\Pages\ListReviewPacks;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Services\ReviewPackService;
|
||||
use App\Support\Audit\AuditActionId;
|
||||
use App\Support\Auth\UiTooltips;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Livewire\Features\SupportTesting\Testable;
|
||||
@ -57,6 +60,8 @@ function createCustomerSafeReviewPackForRbac(ManagedEnvironment $tenant, \App\Mo
|
||||
],
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => hash('sha256', Storage::disk('exports')->get($filePath)),
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
|
||||
@ -214,11 +219,61 @@ function createCustomerSafeReviewPackForRbac(ManagedEnvironment $tenant, \App\Mo
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListReviewPacks::class)
|
||||
->assertTableActionExists('expire', fn (Action $action): bool => $action->isConfirmationRequired(), $pack)
|
||||
->assertTableActionVisible('expire', $pack)
|
||||
->callTableAction('expire', $pack);
|
||||
|
||||
$pack->refresh();
|
||||
expect($pack->status)->toBe(ReviewPackStatus::Expired->value);
|
||||
$audit = AuditLog::query()
|
||||
->where('action', AuditActionId::ReviewPackExpired->value)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($pack->status)->toBe(ReviewPackStatus::Expired->value)
|
||||
->and(Storage::disk('exports')->exists($filePath))->toBeFalse()
|
||||
->and($audit)->not->toBeNull()
|
||||
->and($audit?->resource_type)->toBe('review_pack')
|
||||
->and(data_get($audit?->metadata, 'review_pack_id'))->toBe((int) $pack->getKey())
|
||||
->and(data_get($audit?->metadata, 'artifact_family'))->toBe('review_pack')
|
||||
->and(data_get($audit?->metadata, 'before_status'))->toBe(ReviewPackStatus::Ready->value)
|
||||
->and(data_get($audit?->metadata, 'after_status'))->toBe(ReviewPackStatus::Expired->value)
|
||||
->and(data_get($audit?->metadata, 'file_deleted'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('Spec406 does not delete foreign-disk files when expiring a review pack', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
Storage::fake('public');
|
||||
|
||||
$exportsPath = 'review-packs/exports-same-path.zip';
|
||||
$publicPath = 'review-packs/foreign-disk.zip';
|
||||
Storage::disk('exports')->put($exportsPath, 'PK-exports');
|
||||
Storage::disk('public')->put($publicPath, 'PK-public');
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'file_path' => $publicPath,
|
||||
'file_disk' => 'public',
|
||||
'file_size' => Storage::disk('public')->size($publicPath),
|
||||
'sha256' => hash('sha256', Storage::disk('public')->get($publicPath)),
|
||||
]);
|
||||
|
||||
$expiredPack = app(ReviewPackService::class)->expire($pack, $user, 'spec406_wrong_disk');
|
||||
|
||||
$audit = AuditLog::query()
|
||||
->where('action', AuditActionId::ReviewPackExpired->value)
|
||||
->latest('id')
|
||||
->first();
|
||||
|
||||
expect($expiredPack->status)->toBe(ReviewPackStatus::Expired->value)
|
||||
->and(Storage::disk('public')->exists($publicPath))->toBeTrue()
|
||||
->and(Storage::disk('exports')->exists($exportsPath))->toBeTrue()
|
||||
->and($audit)->not->toBeNull()
|
||||
->and(data_get($audit?->metadata, 'file_disk'))->toBe('public')
|
||||
->and(data_get($audit?->metadata, 'file_deleted'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('disables expire action for readonly member', function (): void {
|
||||
@ -244,6 +299,32 @@ function createCustomerSafeReviewPackForRbac(ManagedEnvironment $tenant, \App\Mo
|
||||
->assertTableActionDisabled('expire', $pack);
|
||||
});
|
||||
|
||||
it('denies direct review pack lifecycle expiration for readonly members', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
|
||||
$filePath = 'review-packs/expire-direct-readonly.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-fake');
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => hash('sha256', Storage::disk('exports')->get($filePath)),
|
||||
]);
|
||||
|
||||
expect(fn (): ReviewPack => app(ReviewPackService::class)->expire($pack, $user, 'spec406_direct'))
|
||||
->toThrow(AuthorizationException::class);
|
||||
|
||||
$pack->refresh();
|
||||
|
||||
expect($pack->status)->toBe(ReviewPackStatus::Ready->value)
|
||||
->and(Storage::disk('exports')->exists($filePath))->toBeTrue()
|
||||
->and(AuditLog::query()->where('action', AuditActionId::ReviewPackExpired->value)->count())->toBe(0);
|
||||
});
|
||||
|
||||
// ─── Signed URL Security ────────────────────────────────────
|
||||
|
||||
it('rejects unsigned download URL with 403', function (): void {
|
||||
|
||||
@ -19,6 +19,7 @@
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperationRunType;
|
||||
use App\Support\ReviewPacks\CustomerOutputGate;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use Filament\Actions\Action;
|
||||
use Filament\Actions\ActionGroup;
|
||||
@ -61,6 +62,21 @@ function getReviewPackHeaderAction(Testable $component, string $name): ?Action
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{file_disk: string, file_path: string, file_size: int, sha256: string}
|
||||
*/
|
||||
function reviewPackResourceArtifactAttributes(string $filePath, string $contents = 'PK-fake-review-pack'): array
|
||||
{
|
||||
Storage::disk('exports')->put($filePath, $contents);
|
||||
|
||||
return [
|
||||
'file_disk' => 'exports',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => hash('sha256', $contents),
|
||||
];
|
||||
}
|
||||
|
||||
function seedReviewPackEvidence(ManagedEnvironment $tenant): EvidenceSnapshot
|
||||
{
|
||||
StoredReport::factory()->create([
|
||||
@ -157,7 +173,7 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
'include_operations' => true,
|
||||
],
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
] + reviewPackResourceArtifactAttributes('review-packs/current-review-pack-'.$review->getKey().'.zip'));
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
@ -417,17 +433,57 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
// ─── Table Row Actions ───────────────────────────────────────
|
||||
|
||||
it('shows the download action for a ready pack', function (): void {
|
||||
\Carbon\Carbon::setTestNow(\Carbon\Carbon::parse('2026-06-24 10:00:00', 'UTC'));
|
||||
|
||||
try {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
$filePath = 'review-packs/test.zip';
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
] + reviewPackResourceArtifactAttributes($filePath));
|
||||
|
||||
$tenant->makeCurrent();
|
||||
Filament::setTenant($tenant, true);
|
||||
$this->actingAs($user);
|
||||
|
||||
$expectedUrl = app(ReviewPackService::class)->generateDownloadUrl(
|
||||
$pack,
|
||||
ReviewPackResource::reviewPackDownloadParameters($pack),
|
||||
);
|
||||
parse_str((string) parse_url($expectedUrl, PHP_URL_QUERY), $queryParameters);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListReviewPacks::class)
|
||||
->assertTableActionVisible('download', $pack)
|
||||
->assertTableActionHasUrl('download', $expectedUrl, $pack);
|
||||
|
||||
$this->get($expectedUrl)->assertOk();
|
||||
|
||||
expect($queryParameters[CustomerOutputGate::INTERNAL_PREVIEW_QUERY_KEY] ?? null)->toBe('1');
|
||||
} finally {
|
||||
\Carbon\Carbon::setTestNow();
|
||||
}
|
||||
});
|
||||
|
||||
it('hides download actions when a ready pack artifact fails validation', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
$filePath = 'review-packs/test.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-fake');
|
||||
$filePath = 'review-packs/tampered.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-tampered');
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'file_disk' => 'exports',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => str_repeat('0', 64),
|
||||
]);
|
||||
|
||||
$tenant->makeCurrent();
|
||||
@ -435,7 +491,11 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ListReviewPacks::class)
|
||||
->assertTableActionVisible('download', $pack);
|
||||
->assertTableActionHidden('download', $pack);
|
||||
|
||||
Livewire::actingAs($user)
|
||||
->test(ViewReviewPack::class, ['record' => $pack->getKey()])
|
||||
->assertActionHidden('download');
|
||||
});
|
||||
|
||||
it('keeps expire grouped under More while still allowing owners to expire a ready pack', function (): void {
|
||||
@ -443,15 +503,11 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||
|
||||
$filePath = 'review-packs/expire-test.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-fake');
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'file_path' => $filePath,
|
||||
'file_disk' => 'exports',
|
||||
]);
|
||||
] + reviewPackResourceArtifactAttributes($filePath));
|
||||
|
||||
$tenant->makeCurrent();
|
||||
Filament::setTenant($tenant, true);
|
||||
@ -613,6 +669,7 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
user: $user,
|
||||
snapshot: $snapshot,
|
||||
review: $review,
|
||||
packOverrides: reviewPackResourceArtifactAttributes('review-packs/stale-source-review-pack.zip', 'PK-stale-source-review-pack'),
|
||||
summaryOverrides: [
|
||||
'review_status' => 'published',
|
||||
'review_completeness_state' => 'complete',
|
||||
@ -687,7 +744,7 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
]);
|
||||
] + reviewPackResourceArtifactAttributes('review-packs/view-header-download.zip', 'PK-view-header-download'));
|
||||
|
||||
$tenant->makeCurrent();
|
||||
Filament::setTenant($tenant, true);
|
||||
@ -736,8 +793,8 @@ function createCurrentReviewPackForResourcePreview(ManagedEnvironment $tenant, \
|
||||
|
||||
[, $pack] = createCurrentReviewPackForResourcePreview($tenant, $user);
|
||||
$pack->forceFill([
|
||||
'sha256' => hash('sha256', 'customer-pack-flow'),
|
||||
'fingerprint' => hash('sha256', 'customer-pack-fingerprint'),
|
||||
...reviewPackResourceArtifactAttributes('review-packs/customer-pack-flow.zip', 'customer-pack-flow'),
|
||||
])->save();
|
||||
$pack = $pack->fresh(['environmentReview']);
|
||||
|
||||
|
||||
@ -16,10 +16,15 @@
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function (): void {
|
||||
Storage::fake('exports');
|
||||
});
|
||||
|
||||
function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $tenant): void
|
||||
{
|
||||
app(SettingsWriter::class)->updateWorkspaceCommercialLifecycle(
|
||||
@ -37,6 +42,21 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{file_disk: string, file_path: string, file_size: int, sha256: string}
|
||||
*/
|
||||
function customerReviewWorkspacePackArtifactAttributes(string $filePath, string $contents = 'PK-customer-review-pack'): array
|
||||
{
|
||||
Storage::disk('exports')->put($filePath, $contents);
|
||||
|
||||
return [
|
||||
'file_disk' => 'exports',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => hash('sha256', $contents),
|
||||
];
|
||||
}
|
||||
|
||||
it('shows a customer-safe download action when the latest released review pack is ready', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
@ -66,7 +86,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
],
|
||||
],
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
] + customerReviewWorkspacePackArtifactAttributes('review-packs/customer-safe-ready.zip', 'PK-customer-safe-ready'));
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
@ -95,6 +115,55 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
expect($component->html())->toMatch('/<h2[^>]*>\s*Ready\s*<\/h2>/');
|
||||
});
|
||||
|
||||
it('does not offer a customer-safe download when the ready pack file fails validation', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
$snapshot = seedEnvironmentReviewEvidence($tenant, findingCount: 0, driftCount: 0);
|
||||
|
||||
$review = composeEnvironmentReviewForTest($tenant, $user, $snapshot);
|
||||
$review->forceFill([
|
||||
'status' => EnvironmentReviewStatus::Published->value,
|
||||
'published_at' => now(),
|
||||
'published_by_user_id' => (int) $user->getKey(),
|
||||
])->save();
|
||||
$review = markEnvironmentReviewCustomerSafeReady($review);
|
||||
|
||||
$filePath = 'review-packs/customer-safe-tampered.zip';
|
||||
Storage::disk('exports')->put($filePath, 'PK-customer-safe-tampered');
|
||||
|
||||
$pack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'environment_review_id' => (int) $review->getKey(),
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'initiated_by_user_id' => (int) $user->getKey(),
|
||||
'options' => [
|
||||
'include_pii' => false,
|
||||
'include_operations' => true,
|
||||
],
|
||||
'expires_at' => now()->addDay(),
|
||||
'file_disk' => 'exports',
|
||||
'file_path' => $filePath,
|
||||
'file_size' => Storage::disk('exports')->size($filePath),
|
||||
'sha256' => str_repeat('0', 64),
|
||||
]);
|
||||
|
||||
$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('Review pack')
|
||||
->assertSee('Not configured')
|
||||
->assertSee('Review Pack has not been generated for this released review yet.')
|
||||
->assertDontSee('Download customer-safe review pack');
|
||||
});
|
||||
|
||||
it('keeps the customer review workspace download action visible while suspended read-only', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create();
|
||||
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'readonly');
|
||||
@ -124,7 +193,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
],
|
||||
],
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
] + customerReviewWorkspacePackArtifactAttributes('review-packs/customer-safe-suspended.zip', 'PK-customer-safe-suspended'));
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
@ -199,7 +268,7 @@ function suspendCustomerReviewWorkspacePackAccessWorkspace(ManagedEnvironment $t
|
||||
'include_operations' => true,
|
||||
],
|
||||
'expires_at' => now()->addDay(),
|
||||
]);
|
||||
] + customerReviewWorkspacePackArtifactAttributes('review-packs/partial-governance-ready.zip', 'PK-partial-governance-ready'));
|
||||
|
||||
$review->forceFill([
|
||||
'current_export_review_pack_id' => (int) $pack->getKey(),
|
||||
|
||||
@ -0,0 +1,94 @@
|
||||
# Requirements Checklist: Spec 406 - Governance Artifact Lifecycle & Retention
|
||||
|
||||
**Feature**: `specs/406-governance-artifact-lifecycle-retention/`
|
||||
**Review date**: 2026-06-23
|
||||
**Scope**: Preparation artifact quality only. No application implementation performed.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
- [x] The selected candidate was directly provided by the operator as Spec 406.
|
||||
- [x] The selected candidate matches manual backlog item `governance-artifact-lifecycle-retention-runtime`.
|
||||
- [x] `docs/product/spec-candidates.md` was reviewed and still reports no safe automatic next-best-prep target.
|
||||
- [x] The candidate aligns with `docs/product/roadmap.md` Governance Artifact Lifecycle & Retention runtime priority.
|
||||
- [x] Completed Spec 267 is treated as read-only historical context and is not modified.
|
||||
- [x] Specs 158, 262, 400, 403, 404, and 405 are read-only context.
|
||||
- [x] No existing `specs/406-governance-artifact-lifecycle-retention/` package existed before preparation.
|
||||
- [x] A different branch named `406-provider-policy-domain-public-taxonomy` is recorded as unrelated.
|
||||
- [x] The smallest slice is lifecycle action, retention, export/download, hold/delete, file/database consistency, audit, tests, browser proof, and final report over existing artifacts.
|
||||
- [x] Close alternatives are deferred instead of hidden inside this package.
|
||||
- [x] Candidate Selection Gate result: PASS as a manual operator-promoted follow-through candidate.
|
||||
|
||||
## Spec Completeness
|
||||
|
||||
- [x] Problem statement is clear and product-oriented.
|
||||
- [x] Business/product value is explicit.
|
||||
- [x] Primary users/operators are named.
|
||||
- [x] Scope fields cover routes/surfaces, ownership, RBAC, and leakage checks.
|
||||
- [x] Functional requirements are testable.
|
||||
- [x] Non-functional requirements cover security, reliability, auditability, performance, deployment, and test governance.
|
||||
- [x] User stories include independent tests and acceptance scenarios.
|
||||
- [x] Edge cases are documented.
|
||||
- [x] Out-of-scope boundaries forbid portal, eDiscovery, compliance claims, report redesign, evidence/currentness rewrite, JSONB migration, and broad audit scope.
|
||||
- [x] Success criteria are measurable.
|
||||
- [x] Assumptions, risks, and open questions are explicit.
|
||||
|
||||
## Constitution And Proportionality
|
||||
|
||||
- [x] Spec Candidate Check is filled out.
|
||||
- [x] Approval class is exactly one class: Core Enterprise.
|
||||
- [x] Score is recorded and above the minimum threshold.
|
||||
- [x] Proportionality Review is completed.
|
||||
- [x] No generic artifact table/entity/source of truth is approved by default.
|
||||
- [x] No broad lifecycle framework, purge platform, export center, compliance taxonomy, or UI framework is approved by default.
|
||||
- [x] Runtime changes are limited to confirmed in-scope lifecycle/action/proof defects over existing artifacts.
|
||||
- [x] The spec requires stopping and updating spec/plan before broader architecture or product scope.
|
||||
|
||||
## Product Surface Contract
|
||||
|
||||
- [x] `docs/product/standards/product-surface-contract.md` is referenced.
|
||||
- [x] No-legacy posture is recorded.
|
||||
- [x] Product Surface Impact is completed for existing artifact/status/download/customer-output surfaces.
|
||||
- [x] Page archetypes are identified as Report Page, Receipt Page, Decision Page, Technical Annex, and Search/Index Page where applicable.
|
||||
- [x] Surface-budget expectations and Technical Annex/deep-link demotion are documented.
|
||||
- [x] Canonical status vocabulary expectations are documented.
|
||||
- [x] Product Surface exceptions are `none planned`.
|
||||
- [x] Browser proof is required and focused.
|
||||
- [x] Human Product Sanity is required.
|
||||
- [x] UI coverage registry review/update or checked no-update rationale is required if rendered existing surfaces materially change.
|
||||
- [x] Implementation-report close-out fields are required.
|
||||
- [x] Completed historical specs are read-only context and must not be rewritten.
|
||||
|
||||
## Plan Completeness
|
||||
|
||||
- [x] Plan identifies PHP/Laravel/Filament/Livewire/Pest/PostgreSQL/Sail/Dokploy context.
|
||||
- [x] Plan names existing runtime code surfaces likely affected if defects are found.
|
||||
- [x] Plan distinguishes Spec 267 read-only lifecycle completion from Spec 406 action/runtime hardening.
|
||||
- [x] Plan includes UI/Product Surface, Filament/Livewire/deployment, shared-pattern, OperationRun, RBAC, audit, storage, and test-governance posture.
|
||||
- [x] Plan defines lifecycle matrix-first implementation.
|
||||
- [x] Plan includes stop conditions.
|
||||
- [x] Plan does not contradict repository architecture or current code truth.
|
||||
|
||||
## Task Completeness
|
||||
|
||||
- [x] Tasks are ordered by preparation, inventory, matrix, tests, implementation, browser proof, and close-out.
|
||||
- [x] Tasks are small and verifiable.
|
||||
- [x] Tasks require tests before runtime fixes where practical.
|
||||
- [x] Tasks include explicit lane classification.
|
||||
- [x] Tasks include Product Surface and Filament output-contract close-out fields.
|
||||
- [x] Tasks require authorization, cross-workspace, customer-safe, evidence/currentness, failure, storage, retention, audit, and file/database consistency proof.
|
||||
- [x] Tasks include focused browser proof and Human Product Sanity.
|
||||
- [x] Tasks include non-goals preventing scope creep.
|
||||
- [x] Tasks include final validation commands and implementation-report completion.
|
||||
|
||||
## Open Questions And Readiness
|
||||
|
||||
- [x] Product decisions about actual deletion support, expired customer access, and hold persistence are recorded as implementation-time decisions handled by matrix classification.
|
||||
- [x] No open question blocks starting the implementation loop because unsafe decisions must become `PRODUCT DECISION REQUIRED` rows rather than invented behavior.
|
||||
- [x] Spec Readiness Gate result: PASS for implementation preparation.
|
||||
|
||||
## Review Outcome
|
||||
|
||||
- [x] Review outcome class: `acceptable-special-case` for a bounded governance artifact lifecycle runtime-hardening gate.
|
||||
- [x] Workflow outcome: `keep`.
|
||||
- [x] Final note location: future implementation report `specs/406-governance-artifact-lifecycle-retention/implementation-report.md`.
|
||||
- [x] No application implementation was performed during preparation.
|
||||
@ -0,0 +1,119 @@
|
||||
# Spec 406 Implementation Report - Governance Artifact Lifecycle & Retention
|
||||
|
||||
## 1. Candidate Gate Result
|
||||
|
||||
Result: `PASS WITH CONDITIONS` candidate selected by operator.
|
||||
|
||||
Spec 406 is approved as a bounded lifecycle hardening slice over existing governance artifacts. It does not approve a generic artifact registry, new portal, export center, legal/eDiscovery claim, purge platform, new navigation entry, or broad lifecycle framework.
|
||||
|
||||
## 2. Dirty State
|
||||
|
||||
- Branch: `406-governance-artifact-lifecycle-retention`
|
||||
- Baseline HEAD: `686947d2 feat: harden json to jsonb data layer for trust payloads (#476)`
|
||||
- Initial working tree: untracked `specs/406-governance-artifact-lifecycle-retention/`; no tracked file modifications before implementation edits.
|
||||
- Initial `git diff --check`: pass.
|
||||
- Completed-spec rewrite assertion: Specs 158, 262, 267, 400, 403, 404, and 405 are read-only context. Their completed task markers, implementation reports, browser proof, and validation history were not edited.
|
||||
|
||||
## 3. Spec 404/405 Carry-Forward
|
||||
|
||||
- Spec 404 remains `PASS WITH CONDITIONS` for external Staging/Dokploy validation. Spec 406 may prove local management-report PDF lifecycle/download behavior, but it does not claim Staging/Dokploy readiness.
|
||||
- Spec 405 remains `PASS WITH CONDITIONS` for external Staging/Dokploy validation. Spec 406 touches no JSON/JSONB conversion path and carries no storage-engine readiness claim beyond existing local test proof.
|
||||
- Neither remaining condition blocks the narrow Spec 406 local hardening of review-pack download consistency, review-pack lifecycle action authorization/audit, and matrix/report truth.
|
||||
|
||||
## 4. Governance Artifact Lifecycle Matrix
|
||||
|
||||
| Family | Current owner/source | File dependency | Customer-safe boundary | Lifecycle/file rules | Hold/delete support | Status | Risk/follow-up |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Review packs | `ReviewPack`, `ReviewPackService`, `ReviewPackDownloadController`, `ReviewPackRenderedReportController`, `ReviewPackResource`, `PruneReviewPacksCommand` | Private `exports` disk, `file_disk`, `file_path`, `file_size`, `sha256` | `CustomerOutputGate` plus signed download/rendered report routes | Ready/non-expired/customer-safe packs may download or render only after current `exports`-disk existence, positive size, size match, and SHA-256 match. Expired/queued/failed/missing/tampered/wrong-disk files fail closed. Spec 406 hardens size/SHA/wrong-disk checks and action audit. | Expire supported now. Hard-delete via prune exists after grace. Hold remains `PRODUCT_DECISION_REQUIRED`; no hold columns/actions added. | `PASS WITH CONDITIONS` | Follow-up required for legal hold/export-before-delete/purge governance. |
|
||||
| Stored reports / management PDFs | `StoredReport`, `ManagementReportPdfService`, `ManagementReportPdfDownloadController`, `StoredReportResource`, `PruneStoredReportsCommand` | Private `exports` disk, PDF bytes, `file_size`, `sha256` | Source review pack must be current, released, customer-safe, and downloadable by actor | Existing controller validates ready metadata, review-pack state/currentness, file existence, size, valid PDF bytes, and SHA before streaming. | Delete via retention prune exists. Hold is `PRODUCT_DECISION_REQUIRED`; no hold columns/actions added. | `PASS WITH CONDITIONS` | Local lifecycle/download proof exists; external Spec 404 Staging/Dokploy condition carries forward. |
|
||||
| Evidence snapshots | `EvidenceSnapshot`, `EvidenceSnapshotService`, `EvidenceSnapshotResource`, evidence currentness/anchor services | Database-backed evidence rows; no downloadable artifact file in this slice | Customer-facing review output consumes summarized/released evidence, not raw evidence by default | Existing service supports expiration and audit; currentness remains separate from review-pack release proof. No file-download lifecycle added. | Expire supported now. Delete/hold/purge are `DEFERRED`. | `PASS WITH EXCEPTION` | Follow-up only if evidence deletion/hold becomes a product decision. |
|
||||
| Customer review retained output | `CustomerReviewWorkspace`, review-pack rendered report/download routes, `CustomerOutputGate` | Review pack ZIP and management PDF when available | Customer/read-only surface must hide raw evidence, OperationRun internals, source keys, fingerprints, and provider payloads | Existing tests cover released/customer-safe visibility, unsafe/internal denial, and customer-safe copy boundaries. Spec 406 inherits this and adds review-pack file consistency proof. | No mutation surface. Hold/delete N/A on customer surface. | `PASS WITH CONDITIONS` | Browser proof still required for representative rendered path. |
|
||||
| OperationRun proof | `OperationRun`, operation detail/resources, OperationRun UX contracts | No artifact file by default | Internal proof only unless an existing product contract exposes derived artifact output | Execution status/outcome remains execution truth and must not become artifact lifecycle truth. No lifecycle mutation implemented here. | Delete/hold/purge `DEFERRED`; not an artifact owner in this slice. | `DEFERRED` | Retain current internal proof posture; no new customer exposure. |
|
||||
| Finding exceptions / accepted risks / governance inbox artifacts | `FindingException`, `FindingExceptionDecision`, finding exception services/resources, customer review summaries | Database-backed decisions and evidence references | Customer review output may show customer-safe accepted-risk summary only; internal rationale remains hidden | Existing workflows authorize lifecycle decisions and audit request/approve/reject/renew/revoke actions. No file-backed download lifecycle in this slice. | Decision lifecycle supported. Delete/hold/purge `DEFERRED`. | `PASS WITH EXCEPTION` | Follow-up only if accepted-risk deletion/retention semantics need dedicated product behavior. |
|
||||
|
||||
## 5. Runtime Changes
|
||||
|
||||
- `ReviewPackService` now owns review-pack file-download validation through `resolveDownloadableArtifact()` / `hasDownloadableArtifact()`. The rule requires `ready`, non-expired lifecycle state, `exports` disk, a present file path, positive stored size, present SHA-256, private-disk existence, exact size match, and SHA-256 byte match before any surface treats a ZIP as downloadable.
|
||||
- `ReviewPackDownloadController` delegates byte validation to `ReviewPackService` and streams only the already validated bytes. Signed URLs continue to re-check current lifecycle, authorization, customer-output gate, file presence, size, disk, and hash at request time.
|
||||
- `ReviewPackRenderedReportController` and `CustomerOutputGate` now use the same downloadable-artifact validation before rendering customer/internal report views or offering the toolbar download URL, so rendered-report signed URLs also fail closed after tampering, missing bytes, wrong disk, or expiry.
|
||||
- `ReviewPackService::expire()` centralizes the ReviewPack expire transition. It requires tenant scope, `REVIEW_PACK_MANAGE`, actor tenant access, ready status, private-file delete verification, and audit proof through `review_pack.expired`.
|
||||
- `ReviewPackResource` and `ViewReviewPack` now hide review-pack download actions when the private artifact fails validation and share the same customer-output/internal-preview URL parameter logic, so visible table downloads do not point at a server-denied stream mode. The destructive expire action remains a Filament action with confirmation, server-side capability enforcement, file delete, and audit logging.
|
||||
- `CustomerReviewWorkspace` now uses the same downloadable-artifact validation for customer-safe download URLs and review-pack availability state, so a ready record with missing/tampered/wrong-disk bytes is not shown as a valid customer download.
|
||||
- `AuditActionId` includes `review_pack.expired` labels/summaries.
|
||||
- No database migrations, new tables, new lifecycle framework, new navigation, new panel, new assets, or new runtime dependencies were added.
|
||||
|
||||
## 6. Tests Added Or Updated
|
||||
|
||||
- `ReviewPackDownloadTest` covers invalid file metadata/bytes (`zero-byte`, size mismatch, SHA mismatch, missing SHA, wrong disk), verifies old signed download URLs re-check current lifecycle before streaming bytes, and verifies rendered-report signed URLs re-check artifact integrity before rendering.
|
||||
- `ReviewPackRbacTest` covers owner expire behavior, confirmation state, export-disk file deletion, wrong-disk non-deletion, audit metadata, and direct service denial for a read-only actor.
|
||||
- `ReviewPackResourceTest` now creates realistic export-disk fixtures for positive download UI tests, covers the table download URL for internal previews, and covers hidden list/detail download actions when a ready pack fails hash validation.
|
||||
- `CustomerReviewWorkspacePackAccessTest` now creates realistic customer-safe artifact fixtures and covers a ready customer-safe pack with tampered bytes being shown as not configured without a download action.
|
||||
- Fixtures remain feature-local. No global artifact matrix harness or broad artifact registry test framework was introduced.
|
||||
|
||||
## 7. Browser Proof
|
||||
|
||||
- Command: `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec379ManagementReportPdfSmokeTest.php`
|
||||
- Result: `1 passed (18 assertions)`, re-run after the ReviewPack table download URL/internal-preview fix.
|
||||
- Surface: existing ReviewPack detail / management-report PDF smoke path.
|
||||
- Actor/role: authorized admin test user from the existing browser smoke.
|
||||
- Artifact type/state: ready ReviewPack detail surface with management-report PDF action state.
|
||||
- Expected/actual: rendered surface remained available and action layout remained valid after ReviewPack download visibility was tightened.
|
||||
- Console/runtime/network: no failing browser assertions; no screenshot artifact emitted on pass.
|
||||
- Held artifact browser proof: `N/A` for Spec 406 implementation. Hold support for review packs, stored reports, OperationRun proof, evidence snapshots, and accepted-risk/finding decision artifacts remains `PRODUCT_DECISION_REQUIRED` or `DEFERRED`; no hold/delete UI was added.
|
||||
- Missing/tampered-file browser proof: covered by Feature/Livewire tests rather than an additional browser proof in this slice; no new visible surface was introduced, only existing download/rendered-report affordances are hidden or denied.
|
||||
|
||||
## 8. Product Surface Close-Out
|
||||
|
||||
- Runtime UI files changed: `ReviewPackResource`, `ViewReviewPack`, and `CustomerReviewWorkspace`.
|
||||
- UI impact: existing ReviewPack download actions and rendered-report toolbar downloads become stricter and disappear when the private artifact is not actually downloadable. Existing customer workspace package availability now fails closed for tampered/missing/wrong-disk ZIPs.
|
||||
- Product Surface exceptions: none.
|
||||
- Human Product Sanity: pass. Purpose remains clear, the dominant next action is unchanged, technical diagnostics remain demoted, and canonical customer labels remain `Ready`, `Running`, `Failed`, `Expired`, `Blocked`, `Needs attention`, and `Not configured`.
|
||||
- Visible complexity outcome: neutral. No new page, navigation entry, panel, card stack, portal, export center, or extra customer-facing action was added.
|
||||
- No-legacy posture: canonical lifecycle hardening over current pre-production behavior.
|
||||
- Route inventory / design coverage matrix: no update. Existing routes/surfaces were tightened; no new rendered route or page archetype was introduced.
|
||||
|
||||
## 9. Filament / Livewire / Deployment Close-Out
|
||||
|
||||
- Livewire v4 compliance: Livewire 4.1.4 confirmed by Laravel Boost application info.
|
||||
- Provider registration location: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; no provider registration changes.
|
||||
- Global search posture: no new globally searchable resource. `ReviewPackResource` remains `$isGloballySearchable = false`; it has a View page, but global search stays disabled.
|
||||
- Destructive/high-impact actions: `expire` remains `Action::make(...)->action(...)` with `requiresConfirmation()`, `REVIEW_PACK_MANAGE` authorization, tenant access checks, file delete verification, and audit proof. No destructive URL-only action was added.
|
||||
- Asset strategy: no new assets, no `FilamentAsset::register()`, and no new on-demand asset. No additional `filament:assets` requirement beyond the existing deployment baseline.
|
||||
- Deployment impact: code-only. No migrations, env vars, queues, scheduler changes, storage volume changes, reverse proxy changes, or Dokploy configuration changes. Existing private `exports` disk must remain persistent and private.
|
||||
|
||||
## 10. Validation Commands
|
||||
|
||||
- `php -l apps/platform/app/Services/ReviewPackService.php` - pass.
|
||||
- `php -l apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` - pass.
|
||||
- `php -l apps/platform/app/Http/Controllers/ReviewPackRenderedReportController.php` - pass.
|
||||
- `php -l apps/platform/app/Support/ReviewPacks/CustomerOutputGate.php` - pass.
|
||||
- `php -l apps/platform/app/Filament/Resources/ReviewPackResource.php` - pass.
|
||||
- `php -l apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php` - pass.
|
||||
- `php -l apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php` - pass.
|
||||
- `php -l apps/platform/tests/Feature/ReviewPack/ReviewPackRbacTest.php` - pass.
|
||||
- `php -l apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php` - pass.
|
||||
- `php -l apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` - pass.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackDownloadTest.php --filter=Spec406` - `7 passed (14 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackRbacTest.php --filter=Spec406` - `1 passed (6 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec406` - `8 passed (20 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php --filter='shows the download action for a ready pack'` - `1 passed (6 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php` - `23 passed (121 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackRbacTest.php tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php` - `67 passed (352 assertions)`.
|
||||
- Earlier focused runs before post-review hardening: `ReviewPackDownloadTest` - `22 passed (129 assertions)`, `ReviewPackResourceTest` - `23 passed (117 assertions)`, `CustomerReviewWorkspacePackAccessTest` - `8 passed (55 assertions)`, `ReviewPackRbacTest` - `12 passed (39 assertions)`, `--filter=Spec406` - `6 passed (12 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec379ManagementReportPdfSmokeTest.php` - `1 passed (18 assertions)`.
|
||||
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - pass.
|
||||
- `git diff --check` - pass.
|
||||
- PostgreSQL lane: `N/A`; no migrations, indexes, or constraints were added.
|
||||
|
||||
## 11. Findings And Gate Result
|
||||
|
||||
- Final gate result: `PASS WITH CONDITIONS`.
|
||||
- Confirmed defect fixed: ready ReviewPack records with missing/tampered/wrong-disk/zero-byte artifacts could still be treated as downloadable by some UI paths or rendered-report signed URLs after URL creation. Runtime now fails closed and UI does not offer those as valid downloads or rendered reports.
|
||||
- Confirmed defect fixed: ReviewPack list downloads for internal-only packs could render a visible action without the required `internal_preview=1` signed URL parameter. List and detail downloads now share the same stream-mode URL logic.
|
||||
- Confirmed defect fixed: ReviewPack expire action logic was page-local and did not emit a dedicated lifecycle audit action. Runtime now uses a service transition with authorization, file delete verification, and `review_pack.expired` audit metadata.
|
||||
- Local residual risk closed: focused browser proof was re-run after the final table-link fix and remains passing.
|
||||
- Remaining condition: legal hold, export-before-delete, broad purge, and cross-family deletion governance remain product decisions. No hold columns/actions were added.
|
||||
- Remaining condition: Spec 404 and Spec 405 external Staging/Dokploy validation conditions carry forward; Spec 406 does not claim production deployment proof.
|
||||
- `PASS` without conditions is intentionally not claimed because SC-406-002 and T061 require `PASS WITH CONDITIONS` when high-risk artifact families remain `DEFERRED` / `PRODUCT_DECISION_REQUIRED` for hold/delete or when Spec 404/405 carry-forward conditions remain unresolved.
|
||||
- Reports/logs/generated artifacts review: no secrets, tokens, raw credential payloads, sensitive provider payloads, private URLs, customer data, or stack traces were added to report/test fixtures.
|
||||
- Post-implementation analysis: changes stayed bounded to existing ReviewPack/customer-workspace surfaces, avoided a generic artifact registry/workflow engine, preserved customer-safe boundaries, and did not rewrite completed historical specs.
|
||||
252
specs/406-governance-artifact-lifecycle-retention/plan.md
Normal file
252
specs/406-governance-artifact-lifecycle-retention/plan.md
Normal file
@ -0,0 +1,252 @@
|
||||
# Implementation Plan: Spec 406 - Governance Artifact Lifecycle & Retention
|
||||
|
||||
**Branch**: `406-governance-artifact-lifecycle-retention` | **Date**: 2026-06-23 | **Spec**: `specs/406-governance-artifact-lifecycle-retention/spec.md`
|
||||
**Input**: Feature specification from `specs/406-governance-artifact-lifecycle-retention/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Prepare a bounded runtime hardening slice for existing governance artifacts. Implementation must inventory review packs, stored reports/management PDFs, evidence snapshots, customer-review outputs, OperationRun proof packages where exposed, and finding/exception artifacts; classify lifecycle behavior; add only necessary current-owner metadata/actions/jobs; prove hold/delete/export/download/file consistency; and produce a final lifecycle matrix. This is not a new artifact portal, purge platform, export center, compliance product, or full browser audit.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15, Laravel 12.52.0, Filament 5.2.1, Livewire 4.1.4.
|
||||
**Primary Dependencies**: Laravel models, migrations, policies/gates, storage disks, queues/scheduler where already used, Filament v5 actions/resources/pages, Pest 4, browser lane.
|
||||
**Storage**: PostgreSQL; private `exports` disk for generated artifacts; existing artifact tables such as `review_packs`, `stored_reports`, `evidence_snapshots`, `operation_runs`, `finding_exceptions`, and related review/output tables.
|
||||
**Testing**: Pest 4 Feature/Filament/Livewire/action tests, focused storage/file tests, targeted scheduler/command tests, focused browser proof.
|
||||
**Validation Lanes**: fast-feedback, confidence, browser; PostgreSQL lane if migrations/constraints/indexes are added.
|
||||
**Target Platform**: TenantPilot Laravel monolith under `apps/platform`, deployed through Sail locally and Dokploy for staging/production.
|
||||
**Constraints**: no new major UI surface, no new panel/provider, no global artifact registry, no broad purge/export framework, no compliance claim, no evidence/currentness rewrite, no PDF layout/template rewrite, no JSONB/data-layer scope, and no completed-spec rewrite.
|
||||
|
||||
## Inherited Baseline
|
||||
|
||||
- Spec 267 delivered the shared read-only lifecycle/retention contract on evidence, tenant-review, review-pack, stored-report, and accepted-risk seams, then explicitly deferred hold and deletion-request persistence.
|
||||
- `ReviewPack` has status, fingerprint, file disk/path/size/SHA, generated/expiry timestamps, evidence/review links, and retention config through `tenantpilot.review_pack`.
|
||||
- `StoredReport` has report type/format/status/profile, payload, file disk/path/size/SHA, generated timestamp, operation/source links, and JSONB payload indexing.
|
||||
- `EvidenceSnapshot` has status, completeness state, summary, fingerprints, generated/expiry timestamps, and links to review packs/environment reviews.
|
||||
- `OperationRun` has separate execution truth through status/outcome/context/summary counts and must not become artifact lifecycle truth.
|
||||
- `PruneReviewPacksCommand` and `PruneStoredReportsCommand` already provide retention-like behavior, but require lifecycle/hold/file consistency proof before broader claims.
|
||||
- Specs 403, 404, and 405 are proof lineage for evidence/currentness, management-report PDF runtime, and JSONB storage. Their contracts must not regress.
|
||||
- Specs 404 and 405 are `PASS WITH CONDITIONS` because external Staging/Dokploy proof remains unavailable. Spec 406 may harden local runtime semantics, but must not claim production/staging readiness for PDF, storage, deployment, or download paths unless new proof is collected or the carry-forward condition is explicitly not applicable.
|
||||
|
||||
## Technical Approach
|
||||
|
||||
1. Record branch, HEAD, dirty state, and `git diff --check`.
|
||||
2. Create `implementation-report.md` before runtime changes and populate the lifecycle matrix as inventory proceeds.
|
||||
3. Inventory in-scope artifact families, actions, routes, policies, commands/jobs, file dependencies, retention config, audit paths, and tests.
|
||||
4. Classify each artifact family as `PASS`, `PASS WITH EXCEPTION`, `MISSING PROOF`, `DEFECT FOUND`, `PRODUCT DECISION REQUIRED`, or `DEFERRED`, and separately classify hold/delete support as `SUPPORTED_NOW`, `DEFERRED`, or `PRODUCT_DECISION_REQUIRED`.
|
||||
5. Add tests before fixes where feasible for hold, delete/archive/expire, export/download, direct authorization, customer-safe output, and file/database consistency.
|
||||
6. Implement only confirmed in-scope lifecycle behavior on existing artifact owners.
|
||||
7. Run focused tests and browser proof.
|
||||
8. Complete implementation-report gate result and next recommendation.
|
||||
|
||||
## Likely Affected Repository Surfaces
|
||||
|
||||
Implementation must verify exact paths before editing.
|
||||
|
||||
```text
|
||||
apps/platform/app/Models/ReviewPack.php
|
||||
apps/platform/app/Models/StoredReport.php
|
||||
apps/platform/app/Models/EvidenceSnapshot.php
|
||||
apps/platform/app/Models/OperationRun.php
|
||||
apps/platform/app/Models/FindingException.php
|
||||
apps/platform/app/Models/FindingExceptionDecision.php
|
||||
apps/platform/app/Filament/Resources/ReviewPackResource.php
|
||||
apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php
|
||||
apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php
|
||||
apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php
|
||||
apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php
|
||||
apps/platform/app/Http/Controllers/ReviewPackDownloadController.php
|
||||
apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php
|
||||
apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
|
||||
apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthEnvelope.php
|
||||
apps/platform/app/Support/Badges/BadgeCatalog.php
|
||||
apps/platform/app/Support/Badges/BadgeRenderer.php
|
||||
apps/platform/app/Support/Badges/Domains/GovernanceArtifactLifecycleBadge.php
|
||||
apps/platform/app/Support/Badges/Domains/GovernanceArtifactRetentionBadge.php
|
||||
apps/platform/app/Support/Audit/AuditActionId.php
|
||||
apps/platform/app/Services/Audit/WorkspaceAuditLogger.php
|
||||
apps/platform/app/Services/ReviewPackService.php
|
||||
apps/platform/app/Services/Evidence/EvidenceSnapshotService.php
|
||||
apps/platform/app/Services/ManagementReports/
|
||||
apps/platform/app/Console/Commands/PruneReviewPacksCommand.php
|
||||
apps/platform/app/Console/Commands/PruneStoredReportsCommand.php
|
||||
apps/platform/config/tenantpilot.php
|
||||
apps/platform/config/filesystems.php
|
||||
apps/platform/database/migrations/
|
||||
apps/platform/tests/
|
||||
```
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: existing lifecycle/download/action/status surfaces.
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**: existing review-pack, evidence, stored-report/PDF, customer-review, and finding/exception artifact surfaces only.
|
||||
- **No-impact class**: N/A.
|
||||
- **Native vs custom classification summary**: native Filament resources/pages plus existing controllers.
|
||||
- **Shared-family relevance**: evidence/report viewers, status messaging, download actions, dangerous lifecycle actions, audit links.
|
||||
- **State layers in scope**: artifact lifecycle, retention, file availability, evidence/currentness, customer-safe availability, execution proof.
|
||||
- **Audience modes in scope**: operator/MSP, customer/read-only, support/platform internal.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: decision-first; diagnostics secondary; support/raw evidence third.
|
||||
- **Raw/support gating plan**: raw evidence, provider payloads, source keys, OperationRun internals, file paths, and technical errors remain hidden or capability-gated.
|
||||
- **One-primary-action / duplicate-truth control**: each surface shows one lifecycle summary and one dominant next action; secondary lifecycle actions move to More/danger placement.
|
||||
- **Handling modes by drift class or surface**: review-mandatory for all changed existing surfaces; exception-required for any new route/nav/page or broad lifecycle framework.
|
||||
- **Repository-signal treatment**: hard-stop-candidate if implementation adds new panel/provider, generic artifact table, portal, export center, or purge framework without spec update.
|
||||
- **Special surface test profiles**: standard-native-filament, shared-detail-family, download/controller, browser.
|
||||
- **Required tests or manual smoke**: feature/action tests plus focused browser proof.
|
||||
- **Exception path and spread control**: none planned. Any product surface exception must be recorded in the implementation report.
|
||||
- **Active feature PR close-out entry**: Guardrail / Product Surface.
|
||||
|
||||
## Product Surface Contract Plan
|
||||
|
||||
- **Product Surface Contract reference**: `docs/product/standards/product-surface-contract.md`.
|
||||
- **No-legacy posture**: canonical lifecycle behavior; no fallback readers, duplicate old labels, hidden compatibility routes, or old lifecycle copy.
|
||||
- **Page archetype and surface budget plan**: Report Page, Receipt Page, Decision Page, Technical Annex, Search/Index Page; budgets expected to pass.
|
||||
- **Technical Annex and deep-link demotion plan**: raw IDs, provider payloads, source keys, detector names, OperationRun internals, file paths, and low-level logs are demoted from customer/product defaults.
|
||||
- **Canonical status vocabulary plan**: map to Ready, Needs attention, Blocked, Running, Failed, Expired, Historical, Superseded, Unknown, and severity vocabulary where visible.
|
||||
- **Product Surface exceptions**: none planned.
|
||||
- **Browser verification plan**: focused browser proof for representative lifecycle state, allowed download/export, blocked customer/internal access, `SUPPORTED_NOW` held-delete block where applicable, and missing-file/not-downloadable state.
|
||||
- **Human Product Sanity plan**: review changed rendered behavior and final implementation report.
|
||||
- **Visible complexity outcome target**: neutral or decreased.
|
||||
- **Implementation report target**: `specs/406-governance-artifact-lifecycle-retention/implementation-report.md`.
|
||||
|
||||
## Filament / Livewire / Deployment Posture
|
||||
|
||||
- **Livewire v4 compliance**: Livewire 4.1.4 confirmed; any changed Filament page/resource remains Livewire v4 compatible.
|
||||
- **Panel provider registration location**: Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`; no provider registration change is planned.
|
||||
- **Global search posture**: no new globally searchable resource is planned. Existing affected resources must either remain non-searchable or have safe View/Edit pages and scoped queries.
|
||||
- **Destructive/high-impact action posture**: delete, hard-delete, purge-like, hold release that enables deletion, archive/expire where availability changes, export, and regeneration-affecting lifecycle actions are high-impact. Filament actions must use `->action(...)`, `->requiresConfirmation()`, policy/gate authorization, and audit proof.
|
||||
- **Asset strategy**: no new assets or `FilamentAsset::register()` work planned. Existing deployment baseline for `php artisan filament:assets` remains unchanged unless implementation adds registered assets, which should stop for spec update.
|
||||
- **Testing plan**: Feature/Filament/Livewire action tests, storage/file tests, command/job tests, direct-route authorization tests, customer-safe output tests, focused browser proof.
|
||||
- **Deployment impact**: possible migrations, config/env additions for retention defaults, scheduler/command behavior, queue/storage implications, and Dokploy staging validation. No Graph scopes or provider credentials expected.
|
||||
|
||||
## RBAC / Security / Audit Plan
|
||||
|
||||
- Workspace membership and managed-environment entitlement remain isolation boundaries.
|
||||
- Wrong workspace/environment/non-member access returns 404.
|
||||
- In-scope capability denial returns 403.
|
||||
- Customer reviewers remain read-only and customer-safe bounded.
|
||||
- UI visibility is not authorization; direct routes/actions must enforce policies/gates.
|
||||
- Audit events must redact secrets, raw provider payloads, stack traces, internal exception bodies, and sensitive file paths.
|
||||
- Lifecycle audit metadata should include actor, workspace, managed environment, artifact family, safe artifact reference, old state, new state, result, and failure reason.
|
||||
|
||||
## Data / Migration Plan
|
||||
|
||||
- Reuse existing columns first: `status`, `expires_at`, file metadata, fingerprints, source links, and generated timestamps.
|
||||
- Add current-table fields only when required for behavior, such as `held_at`, `held_by_user_id`, `hold_reason`, `archived_at`, `deleted_at`, `deleted_by_user_id`, or deletion-request fields.
|
||||
- Hold/delete metadata requires per-family `SUPPORTED_NOW` classification before migration. `DEFERRED` or `PRODUCT_DECISION_REQUIRED` means no partial column, action, retention branch, or controller behavior is added for that family in Spec 406.
|
||||
- Do not add a generic artifact table by default.
|
||||
- If migrations are needed, make them reversible where possible and document PostgreSQL/staging risk.
|
||||
- Add indexes only for proven query paths such as retention scans or held-state lookup.
|
||||
- Do not create compatibility shims unless the spec is updated with an explicit exception.
|
||||
|
||||
## Retention / File Consistency Plan
|
||||
|
||||
- Review `PruneReviewPacksCommand`, `PruneStoredReportsCommand`, `tenantpilot.review_pack.*`, `tenantpilot.stored_reports.*`, and `filesystems.exports`.
|
||||
- File/database consistency must be proven before any ready/downloadable state is shown.
|
||||
- Retention cleanup must skip held artifacts only for families with hold support classified `SUPPORTED_NOW`; deferred families must preserve current behavior and record explicit follow-up/no-runtime-mutation rationale.
|
||||
- Missing/zero-byte/corrupt/wrong-disk files must fail closed.
|
||||
- Delete/archive/expire behavior must define whether files are removed, blocked, or retained.
|
||||
- Signed URLs must re-check current artifact state and file availability at request time.
|
||||
|
||||
## OperationRun / Observability Plan
|
||||
|
||||
- Artifact lifecycle state must not replace `OperationRun.status` or `OperationRun.outcome`.
|
||||
- Existing review-pack/PDF generation OperationRun paths remain shared.
|
||||
- Long-running export/delete/purge-class work should reuse the existing OperationRun start/completion UX. If that work exceeds this slice, split it.
|
||||
- Direct local lifecycle mutations may stay audit-only when they are bounded DB/file operations and not queued/cross-resource.
|
||||
- Final report must classify which lifecycle actions created OperationRuns and which produced audit-only proof.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Feature and Filament/Livewire action tests for behavior; browser for rendered action/status proof; PostgreSQL lane when migrations/indexes are added.
|
||||
- **Affected validation lanes**: fast-feedback, confidence, browser, pgsql conditional.
|
||||
- **Why this lane mix is sufficient**: lifecycle correctness is behavioral and authorization-sensitive, and rendered/browser proof is needed only for representative product-surface and action-state behavior.
|
||||
- **Narrowest proving commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec406`
|
||||
- targeted existing suites for ReviewPack, StoredReport/management PDF, Evidence, CustomerReviewWorkspace, Findings, and OperationRun.
|
||||
- `cd apps/platform && ./vendor/bin/sail php vendor/bin/pest tests/Browser --filter=Spec406 --compact` if a focused browser test is added.
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
- **Fixture/helper cost risks**: moderate; use existing factories and focused fixtures, not a broad artifact matrix harness unless implementation report justifies it.
|
||||
- **Heavy-family additions**: no heavy-governance family planned.
|
||||
- **Browser proof**: required and focused.
|
||||
- **Budget/baseline impact**: record any material browser or retention-job runtime in implementation report.
|
||||
- **Escalation path**: `document-in-feature` for bounded family-local exceptions; `follow-up-spec` for purge/export-before-delete/portal; `reject-or-split` for broad registry/framework scope.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 - Inventory and lifecycle matrix
|
||||
|
||||
Inventory each artifact family, existing states, file dependencies, customer-safe boundaries, policy/capability checks, actions, retention config, commands/jobs, audit proof, tests, and browser coverage. Populate the implementation-report matrix before code edits.
|
||||
|
||||
### Phase 2 - Tests for current gaps
|
||||
|
||||
Add failing tests for confirmed gaps: `SUPPORTED_NOW` held delete block where applicable, unauthorized direct action, cross-workspace access, missing-file download, failed-to-released prevention, expired/current distinction, customer-safe export boundary, and retention job idempotency.
|
||||
|
||||
### Phase 3 - Bounded runtime hardening
|
||||
|
||||
Implement only the smallest confirmed changes on existing artifact owners, services, commands, controllers, and Filament actions. Use current-table metadata where necessary. Stop if a change requires a portal, registry, purge engine, or export center.
|
||||
|
||||
### Phase 4 - UI/browser proof and product sanity
|
||||
|
||||
Run focused browser proof on representative artifact surfaces. Complete Human Product Sanity and record visible complexity outcome.
|
||||
|
||||
### Phase 5 - Final gate and handoff
|
||||
|
||||
Run focused validation, complete implementation-report sections, classify remaining findings, and recommend `PASS`, `PASS WITH CONDITIONS`, or `FAIL`.
|
||||
|
||||
## Stop Conditions
|
||||
|
||||
- A new generic artifact registry table appears necessary.
|
||||
- Irreversible purge or export-before-delete workflow is required for correctness.
|
||||
- A new route/navigation/panel/customer portal is needed.
|
||||
- Legal/compliance claims are required to define behavior.
|
||||
- Lifecycle behavior depends on rewriting completed specs.
|
||||
- Staging/production data compatibility requires shims not approved by this spec.
|
||||
- Spec 404/405 carry-forward conditions are needed for a Spec 406 readiness claim and cannot be proven or ruled not applicable.
|
||||
|
||||
## Rollout Considerations
|
||||
|
||||
- Migrations must be staged and reversible where possible.
|
||||
- Scheduler/command changes need staging validation before production.
|
||||
- Storage changes must account for private `exports` persistence and Dokploy volumes.
|
||||
- Queue/OperationRun changes require worker deployment notes.
|
||||
- Config/env additions must be documented in `.env.example` or deployment docs only if implementation adds them.
|
||||
- Production promotion requires staging validation of lifecycle actions, downloads, retention job behavior, and browser proof.
|
||||
|
||||
## Deferred Follow-ups
|
||||
|
||||
- Retention & Purge Governance v1.
|
||||
- Data Export Before Deletion v1.
|
||||
- Workspace & Tenant Closure Lifecycle v1.
|
||||
- External/customer artifact portal.
|
||||
- Full Browser/UX Runtime Audit after Spec 406 gate.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|---|---|---|
|
||||
| BLOAT-001 - lifecycle/action semantics across several artifact families | Existing artifacts are already customer/audit proof and unsafe lifecycle behavior can delete, expose, or overstate retained proof | Family-local one-off labels and tests cannot prove cross-artifact hold/export/delete/file consistency; a generic artifact registry is wider than current-release truth |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```text
|
||||
specs/406-governance-artifact-lifecycle-retention/
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
├── spec.md
|
||||
├── plan.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
Implementation may add:
|
||||
|
||||
```text
|
||||
specs/406-governance-artifact-lifecycle-retention/implementation-report.md
|
||||
apps/platform/database/migrations/
|
||||
apps/platform/app/... existing artifact owner/service/controller/resource paths
|
||||
apps/platform/tests/... focused Spec406 and existing family tests
|
||||
```
|
||||
|
||||
## Why This Plan Is Narrow Enough
|
||||
|
||||
The repo already has the artifact families, generated-file metadata, signed downloads, retention commands, customer-output gates, evidence/currentness semantics, OperationRun proof, and audit model. Spec 406 uses those seams to prove lifecycle behavior instead of introducing a new product surface or lifecycle platform. Broader purge/export/closure/customer-portal work remains explicitly deferred.
|
||||
344
specs/406-governance-artifact-lifecycle-retention/spec.md
Normal file
344
specs/406-governance-artifact-lifecycle-retention/spec.md
Normal file
@ -0,0 +1,344 @@
|
||||
# Feature Specification: Spec 406 - Governance Artifact Lifecycle & Retention
|
||||
|
||||
**Feature Branch**: `406-governance-artifact-lifecycle-retention`
|
||||
**Created**: 2026-06-23
|
||||
**Status**: Draft / Ready for implementation preparation review
|
||||
**Type**: Product-contract implementation / lifecycle hardening / auditability spec
|
||||
**Input**: User-provided Spec 406 draft, Spec 400 product-contract audit context, Specs 403-405 proof lineage, existing Spec 267 read-only lifecycle contract close-out, `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, and `docs/product/standards/lifecycle-governance.md`.
|
||||
|
||||
## Candidate Selection Context
|
||||
|
||||
- **Selected candidate**: Governance Artifact Lifecycle & Retention.
|
||||
- **Source location**: Direct user-provided Spec 406 draft in `/Users/ahmeddarrazi/.codex/attachments/75e1fbd9-6253-41a7-a47b-634099bd597f/pasted-text.txt`.
|
||||
- **Why selected**: `docs/product/spec-candidates.md` marks the automatic candidate queue as empty, but also lists `governance-artifact-lifecycle-retention-runtime` as manual-promotion backlog item 2. The operator supplied that manual promotion as Spec 406 after Specs 400-405 supplied adjacent product-contract, evidence/currentness, PDF runtime, and JSONB substrate proof lineage. Spec 403 is closed as `PASS`; Specs 404 and 405 are `PASS WITH CONDITIONS` because external Staging/Dokploy proof remains unavailable, so Spec 406 must carry those conditions forward and must not claim production/staging readiness from them.
|
||||
- **Roadmap relationship**: Supports the current trust and auditability hardening lane by proving retained governance artifacts can be released, exported, archived, expired, held, deleted, and downloaded without false evidence claims, file/database drift, customer-safe leakage, or authorization drift.
|
||||
- **Completed-spec guardrail result**:
|
||||
- `specs/267-artifact-lifecycle-retention/` is completed/historical context for the shared read-only lifecycle and retention contract. Its implementation close-out, checked task history, browser smoke, and deferred hold/deletion-request decision must not be rewritten.
|
||||
- Specs 158, 262, 267, 400, 401, 402, 403, 404, and 405 are read-only context. Their validation history, implementation reports, and completed task markers must not be normalized or removed.
|
||||
- No `specs/406-governance-artifact-lifecycle-retention/` package existed before this preparation. A different branch named `406-provider-policy-domain-public-taxonomy` exists, but it has no matching Spec 406 lifecycle package and is unrelated.
|
||||
- **Smallest viable implementation slice**: Inventory existing governance artifact families; create a lifecycle matrix; implement only bounded lifecycle, hold, export/download, delete/archive/expire, and file/database consistency behavior that can be attached to existing artifact owners; add audit/OperationRun proof where repo contracts require it; run focused tests and browser proof against existing surfaces; produce a final implementation report. No new major surface, compliance claim, portal, registry, purge platform, or broad export center is allowed.
|
||||
- **Feature description for Spec Kit**: Prove and minimally harden lifecycle behavior for existing TenantPilot governance artifacts so retained proof can be safely viewed, exported, held, archived, expired, deleted, or blocked according to explicit product rules without weakening evidence truth, customer-safe boundaries, authorization, auditability, or file/database consistency.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: TenantPilot has repo-real evidence snapshots, review packs, stored reports, management-report PDFs, OperationRun proof, findings, exceptions, and customer review outputs, but lifecycle actions and retention behavior remain uneven across artifact families.
|
||||
- **Today's failure**: A generated or released artifact can look downloadable when its file is missing, an expired artifact can be interpreted as current proof, a held artifact can lack enforceable delete blocking, and customer-safe exports can rely on local assumptions instead of a tested lifecycle contract.
|
||||
- **User-visible improvement**: Operators and customer reviewers get honest artifact availability and lifecycle state. Destructive or export actions are authorized, confirmation-backed where visible, audited, and blocked when state or hold rules require it.
|
||||
- **Smallest enterprise-capable version**: Implement lifecycle proof for existing high-risk artifact families only: review packs, stored reports/management PDFs, evidence snapshots, OperationRun proof packages where exposed as artifacts, governance inbox/finding exception artifacts, and current customer-review outputs. Scope includes lifecycle matrix, retention/expiration/hold/delete/export/download gates, tests, browser proof, and final report.
|
||||
- **Explicit non-goals**: No new customer portal, eDiscovery product, legal compliance claim, broad export center, new report template, PDF layout rewrite, evidence/currentness semantics rewrite, JSONB migration, provider integration, backup/restore behavior, authorization model, or full browser/UX/runtime audit.
|
||||
- **Permanent complexity imported**: Possible current-table metadata fields, lifecycle transition service/action code, policy methods, retention command/job adjustments, audit event IDs, targeted tests, focused browser proof, and one implementation report. No generic artifact super-table or universal lifecycle framework is approved by default.
|
||||
- **Why now**: Spec 267 delivered read-only lifecycle truth but deferred hold and deletion-request persistence. Specs 400-405 closed or bounded adjacent blockers; Spec 404/405 conditions are non-blocking only where the lifecycle matrix proves they do not affect Spec 406 storage, download, runtime, or deployment claims. The supplied Spec 406 now targets the remaining retained-artifact runtime risk before a broad browser/UX/runtime audit can be trusted.
|
||||
- **Why not local**: Local fixes on one download route or one resource would not prove cross-artifact hold, export, delete, retention, customer-safe, and file/database consistency semantics.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: New lifecycle/action semantics across several artifact families. Defense: the slice is restricted to existing artifacts and existing surfaces, requires a matrix-first implementation, and forbids new surfaces, generic registries, legal claims, and purge-platform scope.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 11/12**
|
||||
- **Decision**: approve as a bounded runtime hardening and proof package.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
TenantPilot uses governance artifacts as operational, customer, audit, or management proof. The product is not lifecycle-stable until it can answer:
|
||||
|
||||
```text
|
||||
Can TenantPilot prove that governance artifacts have clear lifecycle, retention, export, delete, and hold semantics without weakening evidence truth, authorization, customer-safe output, file/database consistency, or auditability?
|
||||
```
|
||||
|
||||
This spec closes the remaining runtime proof gap over existing artifacts. It does not create a new compliance product. It makes existing retained proof safer and more honest.
|
||||
|
||||
## Product / Business Value
|
||||
|
||||
- Reduces accidental deletion, orphaned generated file, and false downloadable-state risk.
|
||||
- Prevents customer-safe output from exposing internal-only evidence, raw payloads, OperationRun internals, or cross-workspace data.
|
||||
- Gives release reviewers a single lifecycle matrix and final gate result before broader browser/UX/runtime audit work proceeds.
|
||||
- Makes future retention, purge, export-before-delete, and customer-portal work smaller because the high-risk current artifact rules are explicit.
|
||||
|
||||
## Primary Users / Operators
|
||||
|
||||
- Workspace admins managing review packs, evidence, stored reports, and management outputs.
|
||||
- System or support operators reviewing audit and OperationRun proof without exposing internal details to customers.
|
||||
- Customer reviewers consuming released customer-safe artifacts.
|
||||
- Engineering reviewers validating lifecycle, authorization, audit, and retention behavior before productization continues.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace plus managed-environment artifact lifecycle hardening over existing artifact records and existing rendered/download surfaces.
|
||||
- **Primary Routes / Surfaces**:
|
||||
- `ReviewPackResource` list/detail and signed review-pack download route.
|
||||
- Management report PDF generation/download surfaces backed by `StoredReport`.
|
||||
- Evidence snapshot list/detail and evidence overview/currentness surfaces.
|
||||
- Customer review workspace retained-output consumption.
|
||||
- OperationRun detail/proof only where a run is exposed as artifact proof.
|
||||
- Finding exception / accepted-risk / governance inbox artifacts where existing surfaces expose retained proof.
|
||||
- **Data Ownership**:
|
||||
- Governance artifacts remain owned by their current workspace and managed environment records.
|
||||
- Stored reports and generated files remain `StoredReport` records plus private `exports` disk paths.
|
||||
- Review packs remain `ReviewPack` records plus existing file metadata.
|
||||
- Evidence snapshots remain `EvidenceSnapshot` records and related evidence items.
|
||||
- No generic artifact super-table is approved by default.
|
||||
- **RBAC**:
|
||||
- Workspace and managed-environment entitlement are required before artifact visibility, download, export, hold, archive, delete, or lifecycle mutation.
|
||||
- Non-members and wrong-scope actors receive deny-as-not-found (`404`).
|
||||
- In-scope members without capability receive `403`.
|
||||
- Customer reviewers can access only released customer-safe artifacts allowed by the existing customer-output contract.
|
||||
- Destructive/high-impact lifecycle actions require server-side authorization, explicit confirmation when exposed through Filament, audit proof, and hold-state blocking.
|
||||
|
||||
For canonical or mixed-scope surfaces:
|
||||
|
||||
- **Default filter behavior when environment context is active**: Existing route-owned workspace/environment scope remains authoritative. Lifecycle actions must resolve the persisted artifact and its owner, not hidden current context.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Download/export/delete/hold/archive/expire routes and actions must re-check workspace, managed environment, capability, customer-safe state, artifact lifecycle state, and file existence at execution time.
|
||||
|
||||
## No Legacy / No Backward Compatibility Constraint *(mandatory)*
|
||||
|
||||
TenantPilot is pre-production for this lifecycle hardening.
|
||||
|
||||
- **Compatibility posture**: canonical lifecycle behavior over current artifact records.
|
||||
- **Legacy aliases, fallback readers, hidden routes, duplicate UI, old labels, or historical fixtures kept?**: no, unless this spec is updated with an explicit compatibility exception.
|
||||
- **Why clean replacement is safe now**: lifecycle action behavior is not yet productized as a production customer contract. Existing historical specs remain read-only context, and current runtime behavior may be tightened before production.
|
||||
|
||||
## UI Surface Impact *(mandatory - UI-COV-001)*
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [ ] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [x] New modal/drawer/wizard/action added
|
||||
- [x] New table/form/state added
|
||||
- [x] Customer-facing surface changed
|
||||
- [x] Dangerous action changed
|
||||
- [x] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage
|
||||
|
||||
- **Route/page/surface**: Existing review-pack, stored-report/management-PDF, evidence, customer-review, OperationRun proof, and finding/exception artifact surfaces.
|
||||
- **Current or new page archetype**: Report Page, Receipt Page, Decision Page, Technical Annex, and Search/Index Page depending on the existing surface.
|
||||
- **Design depth**: Domain Pattern Surface for existing artifact owner surfaces; Technical Annex for OperationRun/raw proof detail.
|
||||
- **Repo-truth level**: repo-verified for review packs, stored reports, evidence snapshots, OperationRun, findings, and current customer review output.
|
||||
- **Existing pattern reused**: native Filament resources/pages, existing signed download controllers, shared artifact-truth/badge paths, current policies/capabilities, existing audit logging, private `exports` disk.
|
||||
- **New pattern required**: none by default. Any new lifecycle action must be attached to an existing artifact owner surface.
|
||||
- **Screenshot required**: yes for focused lifecycle/browser proof on representative rendered surfaces.
|
||||
- **Page audit required**: no full page audit; only focused product-surface proof.
|
||||
- **Customer-safe review required**: yes for released/downloadable/customer-review artifacts.
|
||||
- **Dangerous-action review required**: yes for delete, hard-delete, purge-like, hold release when it enables deletion, and lifecycle actions that change availability.
|
||||
- **Coverage files updated or explicitly not needed**:
|
||||
- [x] `docs/ui-ux-enterprise-audit/route-inventory.md` - review/update required if implementation materially changes a listed route, action, status, or customer-output surface; otherwise record checked no-update rationale in the implementation report.
|
||||
- [x] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md` - review/update required if implementation materially changes visible complexity, page archetype, dangerous-action posture, or customer-safe output; otherwise record checked no-update rationale in the implementation report.
|
||||
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||
- [ ] `N/A - no reachable UI surface impact`
|
||||
- **No-impact rationale when applicable**: N/A. Existing surfaces can change; implementation must either update the registry artifacts above or record a checked no-update rationale when the change is non-material to route inventory and design coverage.
|
||||
|
||||
## Product Surface Impact *(mandatory)*
|
||||
|
||||
Reference: `docs/product/standards/product-surface-contract.md`.
|
||||
|
||||
- **Product Surface Contract applies?**: yes, because existing rendered status, report, download, customer-output, and dangerous-action behavior may change.
|
||||
- **Page archetype**: Report Page for customer/management outputs, Receipt Page for generated artifact proof, Decision Page for lifecycle action confirmation, Technical Annex for OperationRun/internal evidence, Search/Index Page for artifact lists.
|
||||
- **Primary user question**: "Can this artifact still be trusted, used, exported, downloaded, held, or deleted?"
|
||||
- **Primary action**: One artifact-appropriate action such as `Download artifact`, `View source review`, `Place hold`, `Release hold`, or `Delete artifact`.
|
||||
- **Surface budget result**: pass expected; any exception must be documented in the implementation report.
|
||||
- **Technical Annex / deep-link demotion**: OperationRun links, raw evidence IDs, source keys, detectors, fingerprints, provider payloads, raw file paths, and technical logs stay out of customer-facing defaults and remain secondary/internal.
|
||||
- **Canonical status vocabulary**: Use Product Surface states such as Ready, Needs attention, Blocked, Running, Failed, Expired, Historical, Superseded, and Unknown. Domain lifecycle values may exist internally but must map before product display.
|
||||
- **Visible complexity impact**: neutral or decreased; increased visible complexity requires an approved exception.
|
||||
- **Product Surface exceptions**: none planned.
|
||||
|
||||
## Browser Verification Plan *(mandatory)*
|
||||
|
||||
- **Browser proof required?**: yes.
|
||||
- **No-browser rationale**: N/A.
|
||||
- **Focused path when required**: representative review-pack detail/download, management-report PDF owner/download state, evidence snapshot/detail, and customer review workspace retained-output path.
|
||||
- **Primary interaction to execute**: view lifecycle state, attempt allowed download/export, attempt blocked customer/unauthorized access, attempt delete on held artifact, and verify expired/deleted/missing-file artifacts are not presented as valid downloads.
|
||||
- **Console, Livewire, Filament, network, and 500-error checks**: planned.
|
||||
- **Full-suite failure triage**: unrelated failures may be documented only when focused Spec 406 proof is green and evidence supports the classification.
|
||||
|
||||
## Human Product Sanity Check *(mandatory)*
|
||||
|
||||
- **Required?**: yes.
|
||||
- **No-human-sanity rationale**: N/A.
|
||||
- **Reviewer questions**: purpose clear, exactly one dominant next action per surface, technical details demoted, status labels canonical, customer-safe boundary understandable, destructive action risk clear, complexity not worse.
|
||||
- **Planned result location**: `specs/406-governance-artifact-lifecycle-retention/implementation-report.md`.
|
||||
|
||||
## Product Surface Merge Gate Checklist *(mandatory)*
|
||||
|
||||
- [x] No-legacy posture or approved exception recorded.
|
||||
- [x] Product Surface Impact is completed.
|
||||
- [x] Browser proof is planned for rendered lifecycle/download/action behavior.
|
||||
- [x] Human Product Sanity is planned.
|
||||
- [x] Product Surface exceptions are `none planned`.
|
||||
- [x] Implementation report will state Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible complexity outcome, and no completed-spec rewrite assertion.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-406-001**: The implementation MUST inventory all in-scope governance artifact families and produce a Governance Artifact Lifecycle Matrix before lifecycle code changes.
|
||||
- **FR-406-002**: Every in-scope artifact family MUST have a lifecycle classification or a documented exception.
|
||||
- **FR-406-003**: Lifecycle state, retention state, evidence/currentness truth, and file availability MUST remain separate dimensions.
|
||||
- **FR-406-004**: A released artifact MUST remain point-in-time proof and MUST NOT silently become current runtime evidence after underlying evidence changes.
|
||||
- **FR-406-005**: A generated or ready artifact MUST NOT be downloadable when its required file is missing, zero-byte, corrupt, on the wrong disk, or not authorized for the current actor.
|
||||
- **FR-406-006**: A failed artifact MUST NOT be released or displayed as valid generated proof without successful regeneration.
|
||||
- **FR-406-007**: For every artifact family classified as supporting hold in Spec 406, a held artifact MUST NOT be deleted, purged, or automatically cleaned up while the hold is active.
|
||||
- **FR-406-008**: Hold, unhold, archive, expire, delete, export, and download behavior MUST be authorized directly and tested for positive, negative, cross-workspace, cross-environment, and customer-reviewer cases where the action exists.
|
||||
- **FR-406-009**: Destructive or high-impact Filament actions MUST execute via `Action::make(...)->action(...)`, include `->requiresConfirmation()`, and enforce server-side authorization.
|
||||
- **FR-406-010**: Delete behavior MUST be explicit for each artifact family before adding runtime mutation code: archive-only, soft delete, file delete, metadata delete, hard delete, or deferred/no-delete.
|
||||
- **FR-406-011**: If the repo has no purge concept for a family, this spec MUST NOT invent broad purge semantics. Any purge-like need must be recorded as a follow-up or explicit family-local decision.
|
||||
- **FR-406-012**: Export and download paths MUST exclude internal-only evidence, raw provider payloads, raw source keys, internal OperationRun details, stack traces, internal exception messages, system-only links, and cross-workspace data from customer-safe output.
|
||||
- **FR-406-013**: Retention and expiration behavior MUST use existing configuration where present and add only bounded configurable defaults where necessary.
|
||||
- **FR-406-014**: Retention cleanup or expiration jobs MUST be idempotent, skip held artifacts, not cross workspace/environment boundaries, and record audit/OperationRun proof where existing contract requires it.
|
||||
- **FR-406-015**: File/database consistency MUST be tested for generated/ready, failed, expired, deleted, missing-file, held, and customer-safe download cases.
|
||||
- **FR-406-016**: Lifecycle-sensitive actions MUST write audit proof with actor, workspace, managed environment where applicable, artifact family, safe artifact reference, old state, new state, result, and failure reason where applicable.
|
||||
- **FR-406-017**: OperationRun proof MUST remain internal proof unless the existing product contract explicitly allows customer exposure.
|
||||
- **FR-406-018**: The implementation MUST NOT add a new navigation entry, panel, artifact portal, export center, legal/eDiscovery product, broad lifecycle framework, new compliance claim, or new report template.
|
||||
- **FR-406-019**: The implementation MUST preserve Spec 403 evidence/currentness semantics, Spec 404 PDF runtime gate semantics, Spec 405 JSONB storage semantics, and Spec 267 read-only lifecycle close-out history.
|
||||
- **FR-406-020**: A final implementation report MUST include the Governance Artifact Lifecycle Matrix, tests, browser proof, residual findings, gate result, and recommended next step.
|
||||
- **FR-406-021**: Before any hold/delete/retention mutation is implemented, the lifecycle matrix MUST classify each in-scope artifact family's hold/delete support as `SUPPORTED_NOW`, `DEFERRED`, or `PRODUCT_DECISION_REQUIRED`; families classified `DEFERRED` or `PRODUCT_DECISION_REQUIRED` MUST NOT receive partial runtime behavior inside Spec 406.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **Security**: Access remains workspace and managed-environment scoped; customer-safe output must not expose internal-only data.
|
||||
- **Reliability**: Lifecycle transitions must be transactionally safe where database and file operations interact, and failures must not leave false downloadable states.
|
||||
- **Auditability**: Lifecycle actions must produce safe audit metadata with no secrets or raw sensitive payloads.
|
||||
- **Performance**: Retention scans must use existing indexes or justified query-backed indexes only.
|
||||
- **Deployment**: Any migration, queue, scheduler, storage, config, or env impact must be documented for Sail, staging, and Dokploy production promotion.
|
||||
- **Test governance**: Use focused Feature/Filament/Pest/browser coverage; do not create a broad heavy-governance family unless the implementation report justifies it.
|
||||
|
||||
## User Stories And Acceptance Scenarios
|
||||
|
||||
### User Story 1 - Operator understands artifact lifecycle and allowed actions (Priority: P1)
|
||||
|
||||
As a workspace admin, I need each artifact surface to state whether a retained artifact is current, released, historical, archived, expired, deleted, held, failed, or unavailable so I do not rely on false proof.
|
||||
|
||||
**Independent Test**: Given representative review-pack, stored-report/PDF, evidence, and customer-review artifacts, a permitted operator can identify lifecycle state, file availability, and the one allowed or blocked next action without opening raw diagnostics.
|
||||
|
||||
### User Story 2 - Destructive lifecycle actions are blocked or audited correctly (Priority: P1)
|
||||
|
||||
As an operator or reviewer, I need hold/delete/archive/expire actions to enforce policy, confirmation, hold blocking where the family is classified `SUPPORTED_NOW`, file consistency, and audit proof so TenantPilot cannot silently destroy retained proof.
|
||||
|
||||
**Independent Test**: For families classified `SUPPORTED_NOW` for hold, a held artifact cannot be deleted through UI or direct execution; unauthorized and cross-workspace actors are blocked; an allowed delete/archive/expire action records audit proof and does not leave accessible orphan files.
|
||||
|
||||
### User Story 3 - Customer-safe exports and downloads remain bounded (Priority: P1)
|
||||
|
||||
As a customer reviewer, I need released customer-safe artifacts to be downloadable only when valid, released, and safe, while unreleased/internal/deleted/missing-file artifacts stay unavailable.
|
||||
|
||||
**Independent Test**: Customer reviewer can access a released customer-safe artifact, cannot access unreleased/internal artifacts, and cannot download deleted, expired-without-access, failed, or missing-file artifacts.
|
||||
|
||||
### User Story 4 - Retention behavior is deterministic (Priority: P2)
|
||||
|
||||
As a release reviewer, I need retention jobs/actions to expire or archive eligible artifacts deterministically, skip held artifacts where hold is classified `SUPPORTED_NOW`, and document any product decision gaps before purge/export-before-delete work.
|
||||
|
||||
**Independent Test**: Retention logic marks only eligible artifacts, skips held artifacts for `SUPPORTED_NOW` hold families, remains idempotent, and records explicit exceptions or missing decisions instead of inventing broad purge behavior.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- File exists but database state is failed or deleted.
|
||||
- Database state is generated/ready but file is missing, zero-byte, corrupt, or on a wrong disk.
|
||||
- Released review/report references older evidence after current evidence changes.
|
||||
- Held artifact also has deletion requested or is past retention.
|
||||
- Customer reviewer attempts direct route access to unreleased internal artifact.
|
||||
- Workspace is suspended/read-only but retained artifacts should remain readable.
|
||||
- OperationRun failed or cancelled but an artifact was partially generated.
|
||||
- Retention job fails after updating metadata but before file deletion.
|
||||
- Existing signed URL is used after artifact state changed.
|
||||
|
||||
## Data And Truth Source Requirements
|
||||
|
||||
- **Execution truth**: `OperationRun` status/outcome remains execution proof, not artifact lifecycle truth.
|
||||
- **Artifact truth**: `ReviewPack`, `StoredReport`, `EvidenceSnapshot`, and related current artifact owners remain artifact truth sources.
|
||||
- **File truth**: private storage existence, size, hash, and disk/path metadata must agree before download.
|
||||
- **Evidence truth**: Spec 403 currentness/released proof semantics remain authoritative.
|
||||
- **Customer-safe truth**: existing customer-output gates remain authoritative and must be tested for lifecycle paths.
|
||||
- **Retention truth**: existing config such as `tenantpilot.review_pack.retention_days`, `tenantpilot.review_pack.hard_delete_grace_days`, and `tenantpilot.stored_reports.retention_days` is reused before new config is considered.
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **New source of truth?**: possibly, only if existing artifact families lack necessary lifecycle metadata.
|
||||
- **New persisted entity/table/artifact?**: no generic artifact table approved by default; current-table metadata only where necessary.
|
||||
- **New abstraction?**: possibly a bounded lifecycle service/action layer if current family-local code cannot safely coordinate state, file, authorization, and audit.
|
||||
- **New enum/state/reason family?**: possibly bounded lifecycle/retention values, but only when each value changes allowed actions, retention behavior, audit responsibility, or customer visibility.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: retained artifacts can become misleading or unsafe when lifecycle action, file, export, and retention behavior are implicit.
|
||||
- **Existing structure is insufficient because**: Spec 267 delivered read-only lifecycle truth and explicitly deferred hold/deletion persistence; current retention/file/delete behavior is still family-local and not proven across high-risk artifacts.
|
||||
- **Narrowest correct implementation**: matrix-first classification, current artifact owners, family-local metadata where necessary, existing Filament/controller surfaces, and focused tests/browser proof.
|
||||
- **Ownership cost**: lifecycle service/actions, audit IDs, targeted tests, possible migrations, scheduler/deploy notes, and final report upkeep.
|
||||
- **Alternative intentionally rejected**: generic artifact registry, universal purge engine, broad export center, legal hold/eDiscovery product, and full UI audit.
|
||||
- **Release truth**: current-release runtime hardening over already-existing retained artifacts.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `docs/product/spec-candidates.md`
|
||||
- `docs/product/roadmap.md`
|
||||
- `docs/product/standards/lifecycle-governance.md`
|
||||
- `docs/product/standards/product-surface-contract.md`
|
||||
- `specs/158-artifact-truth-semantics/`
|
||||
- `specs/262-lifecycle-governance-taxonomy/`
|
||||
- `specs/267-artifact-lifecycle-retention/`
|
||||
- `specs/400-product-contract-spec-completeness-audit/`
|
||||
- `specs/403-evidence-anchor-currentness-runtime-closure/`
|
||||
- `specs/404-management-report-pdf-staging-validation/`
|
||||
- `specs/405-json-to-jsonb-data-layer-hardening/`
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 267 read-only lifecycle truth is implemented/historical and will not be rewritten.
|
||||
- Specs 404 and 405 are predecessor proof lineage with `PASS WITH CONDITIONS`, not unconditional production/staging readiness. The Spec 406 implementation report must state whether each remaining Staging/Dokploy condition affects the touched artifact family, download path, storage path, runtime path, or deployment claim.
|
||||
- Review packs and stored reports remain the primary generated-file artifact families in this slice.
|
||||
- Customer-facing download/export behavior must reuse existing customer-output gates.
|
||||
- No live production data requires compatibility shims unless implementation discovers and documents an explicit exception.
|
||||
- Actual retention values remain configurable product settings, not legal compliance claims.
|
||||
|
||||
## Risks
|
||||
|
||||
- Hold/delete behavior may require migrations on multiple existing artifact tables.
|
||||
- Retention cleanup can create file/database drift if not transactionally and operationally tested.
|
||||
- Customer-safe export/download behavior can leak internal evidence if raw proof layers are not demoted.
|
||||
- A broad purge/export-before-delete workflow may be discovered but must be split rather than hidden in this slice.
|
||||
- Browser proof may require representative fixtures for multiple artifact families.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Which artifact families should support actual deletion in Spec 406 versus archive/expire-only behavior?
|
||||
- Do customer reviewers retain access to expired released artifacts, or should expired access always be blocked unless a specific existing contract permits it?
|
||||
- Does any artifact family require true hold persistence now, or is hold limited to review packs/stored reports until a later purge/export-before-delete spec?
|
||||
|
||||
These are implementation-time product decisions. They do not block preparation or inventory because the tasks require matrix classification and explicit `SUPPORTED_NOW`, `DEFERRED`, or `PRODUCT_DECISION_REQUIRED` rows before runtime mutation. They do block runtime mutation for affected families until the classification is recorded.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-406-001**: Every in-scope artifact family has a lifecycle matrix row with status, risk, test proof, browser proof, and follow-up.
|
||||
- **SC-406-002**: For every artifact family classified `SUPPORTED_NOW` for hold, held artifacts cannot be deleted by UI, direct action, or retention cleanup; any high-risk family left `DEFERRED` or `PRODUCT_DECISION_REQUIRED` is reported and prevents a stronger result than `PASS WITH CONDITIONS`.
|
||||
- **SC-406-003**: Deleted, failed, expired-without-access, or missing-file artifacts cannot be downloaded as valid proof.
|
||||
- **SC-406-004**: Customer-safe downloads/exports are proven not to expose internal-only proof.
|
||||
- **SC-406-005**: Authorization and cross-workspace/cross-environment denial are tested for lifecycle-sensitive actions.
|
||||
- **SC-406-006**: Audit proof exists for lifecycle actions according to existing TenantPilot conventions.
|
||||
- **SC-406-007**: Focused browser proof passes for representative existing rendered surfaces.
|
||||
- **SC-406-008**: Final gate result is `PASS`, `PASS WITH CONDITIONS`, or `FAIL` according to remaining P0/P1 lifecycle risk.
|
||||
|
||||
## Follow-up Spec Candidates
|
||||
|
||||
- Retention & Purge Governance v1 for irreversible purge, purge scheduling, purge proof, and export-before-delete preconditions.
|
||||
- Data Export Before Deletion v1 for customer-owned export bundles and completion proof.
|
||||
- Workspace & Tenant Closure Lifecycle v1 for broader workspace/managed-environment closure behavior.
|
||||
- External/customer artifact portal only if a later product decision requires a new consumption surface.
|
||||
- Full Browser/UX Runtime Audit after Spec 406 returns `PASS` or acceptable `PASS WITH CONDITIONS`.
|
||||
|
||||
## Implementation Report Requirement
|
||||
|
||||
The implementation loop must create `specs/406-governance-artifact-lifecycle-retention/implementation-report.md` with:
|
||||
|
||||
- Candidate gate result.
|
||||
- Dirty state before/after.
|
||||
- Governance Artifact Lifecycle Matrix.
|
||||
- Spec 404/405 condition carry-forward assessment.
|
||||
- Per-family hold/delete support classification.
|
||||
- Runtime changes and migrations.
|
||||
- Lifecycle rules implemented.
|
||||
- Tests and browser proof.
|
||||
- Authorization/customer-safe proof.
|
||||
- File/database consistency proof.
|
||||
- Retention/hold/delete proof.
|
||||
- Remaining P0/P1/P2/P3 findings.
|
||||
- Deferred items.
|
||||
- Validation commands.
|
||||
- Recommended next step.
|
||||
173
specs/406-governance-artifact-lifecycle-retention/tasks.md
Normal file
173
specs/406-governance-artifact-lifecycle-retention/tasks.md
Normal file
@ -0,0 +1,173 @@
|
||||
# Tasks: Spec 406 - Governance Artifact Lifecycle & Retention
|
||||
|
||||
**Input**: `specs/406-governance-artifact-lifecycle-retention/spec.md`, `plan.md`, `checklists/requirements.md`, user-provided Spec 406 draft, Spec 400 audit context, Specs 403-405 proof lineage, completed Spec 267 lifecycle close-out, and current repo truth.
|
||||
|
||||
**Tests**: Required. This is runtime lifecycle/action hardening over existing governance artifacts. Use Pest 4 Feature/Filament/Livewire action tests, focused storage/file tests, PostgreSQL lane if migrations/indexes are added, and focused browser proof for rendered lifecycle/download/action behavior.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is Feature/Filament/Livewire + focused Browser, with PostgreSQL only when migrations/indexes are added.
|
||||
- [x] New or changed tests stay in the smallest honest family and avoid broad heavy-governance expansion.
|
||||
- [x] Fixtures remain explicit and feature-local; no new global artifact matrix harness unless justified in `implementation-report.md`.
|
||||
- [x] Planned validation commands cover lifecycle behavior without claiming a full browser/UX/runtime audit.
|
||||
- [x] Browser proof is required for representative existing rendered surfaces.
|
||||
- [x] Human Product Sanity and Product Surface close-out are recorded.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the implementation report.
|
||||
|
||||
## Phase 1: Preparation And Safety
|
||||
|
||||
**Purpose**: Establish repo safety, read the package, and prevent completed-spec rewrites.
|
||||
|
||||
- [x] T001 Read `specs/406-governance-artifact-lifecycle-retention/spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`.
|
||||
- [x] T002 Record current branch, HEAD, dirty state, tracked changed files, untracked files, and `git diff --check` in `specs/406-governance-artifact-lifecycle-retention/implementation-report.md`.
|
||||
- [x] T003 Re-read `AGENTS.md`, `.specify/memory/constitution.md`, `docs/ai-coding-rules.md`, `docs/architecture-guidelines.md`, `docs/security-guidelines.md`, `docs/testing-guidelines.md`, `docs/product/standards/product-surface-contract.md`, and `docs/product/standards/lifecycle-governance.md`.
|
||||
- [x] T004 Re-read Specs 158, 262, 267, 400, 403, 404, and 405 as read-only context; record which constraints carry forward and explicitly note Spec 404/405 `PASS WITH CONDITIONS` caveats.
|
||||
- [x] T005 Confirm completed Spec 267 implementation close-out, checked task history, browser proof, and deferred mutation decision are not edited, normalized, unchecked, or removed.
|
||||
- [x] T006 Create `specs/406-governance-artifact-lifecycle-retention/implementation-report.md` with the sections required by `spec.md`.
|
||||
|
||||
## Phase 2: Artifact Inventory And Lifecycle Matrix
|
||||
|
||||
**Purpose**: Prove every lifecycle decision is intentional before runtime edits.
|
||||
|
||||
- [x] T007 Inventory review-pack lifecycle, retention, file, download, audit, and prune behavior in `apps/platform/app/Models/ReviewPack.php`, `apps/platform/app/Services/ReviewPackService.php`, `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`, `apps/platform/app/Console/Commands/PruneReviewPacksCommand.php`, `apps/platform/config/tenantpilot.php`, and existing ReviewPack tests.
|
||||
- [x] T008 Inventory stored-report and management-PDF lifecycle, file, download, status, audit, prune, and runtime-gate behavior in `apps/platform/app/Models/StoredReport.php`, `apps/platform/app/Http/Controllers/ManagementReportPdfDownloadController.php`, management report services, `apps/platform/app/Console/Commands/PruneStoredReportsCommand.php`, and existing Spec379/Spec404 tests.
|
||||
- [x] T009 Inventory evidence snapshot lifecycle, currentness, retention, review-pack linkage, audit, and generated-state behavior in `apps/platform/app/Models/EvidenceSnapshot.php`, `apps/platform/app/Services/Evidence/EvidenceSnapshotService.php`, `EvidenceSnapshotResource`, and existing evidence tests.
|
||||
- [x] T010 Inventory customer-review retained-output access in `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, related review-pack/review models, and customer-workspace tests.
|
||||
- [x] T011 Inventory OperationRun proof package exposure in `apps/platform/app/Models/OperationRun.php`, operation detail surfaces, and existing OperationRun tests without treating execution status as artifact lifecycle truth.
|
||||
- [x] T012 Inventory finding, risk exception, accepted-risk decision, and governance inbox artifact behavior in `FindingException`, `FindingExceptionDecision`, related resources/services, and findings tests.
|
||||
- [x] T013 Populate the Governance Artifact Lifecycle Matrix in `implementation-report.md` with each artifact type, model/table, file dependency, scope, customer-safe boundary, lifecycle fields, allowed states/actions, authorization, retention, Spec 404/405 condition impact, hold/delete/export/audit/test/browser proof, hold/delete support classification, status, risk, and follow-up.
|
||||
- [x] T014 Mark every artifact family as `PASS`, `PASS WITH EXCEPTION`, `MISSING PROOF`, `DEFECT FOUND`, `PRODUCT DECISION REQUIRED`, or `DEFERRED`, and mark hold/delete support as `SUPPORTED_NOW`, `DEFERRED`, or `PRODUCT_DECISION_REQUIRED`.
|
||||
- [x] T015 Stop before runtime edits if any high-risk artifact family lacks lifecycle classification, hold/delete support classification, owner, authorization decision, file-consistency rule, and risk rating.
|
||||
|
||||
## Phase 3: User Story 1 - Operator understands artifact lifecycle and allowed action (Priority: P1)
|
||||
|
||||
**Goal**: Existing artifact surfaces state lifecycle state, retention/file availability, and one allowed or blocked next action without exposing raw diagnostics by default.
|
||||
|
||||
**Independent Test**: A permitted operator views representative review-pack, stored-report/PDF, evidence, and customer-review artifacts and can identify lifecycle state, retention/file availability, customer-safe state, and next action from existing surfaces.
|
||||
|
||||
### Tests for User Story 1
|
||||
|
||||
- [x] T016 [P] [US1] Add or update focused tests under `apps/platform/tests/Feature/ReviewPack/` proving ready, expired, failed, deleted/blocked, missing-file, and historical review-pack state summaries.
|
||||
- [ ] T017 [P] [US1] Add or update focused tests under `apps/platform/tests/Feature/ManagementReports/` or existing Spec404/StoredReport suites proving management-PDF `StoredReport` lifecycle/download state and missing-file failure behavior.
|
||||
- [ ] T018 [P] [US1] Add or update focused tests under `apps/platform/tests/Feature/Evidence/` proving evidence snapshot current, historical, expired, failed/missing, and linked-review artifact state.
|
||||
- [x] T019 [P] [US1] Add or update focused tests for `CustomerReviewWorkspace` proving customer/read-only lifecycle wording and absence of raw/internal artifact details.
|
||||
|
||||
### Implementation for User Story 1
|
||||
|
||||
- [ ] T020 [US1] Update existing artifact-truth/status rendering through `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthEnvelope.php`, `apps/platform/app/Support/Badges/BadgeCatalog.php`, `apps/platform/app/Support/Badges/BadgeRenderer.php`, `apps/platform/app/Support/Badges/Domains/GovernanceArtifactLifecycleBadge.php`, `apps/platform/app/Support/Badges/Domains/GovernanceArtifactRetentionBadge.php`, and existing resource schemas so `ReviewPackResource`, `ViewReviewPack`, `EvidenceSnapshotResource`, `ViewEvidenceSnapshot`, and `CustomerReviewWorkspace` show lifecycle and next-action truth without page-local vocabulary drift.
|
||||
- [ ] T021 [US1] Update `StoredReport`/management-PDF owner surfaces or controller responses so file-backed readiness is truthful and missing/invalid files are not offered as valid downloads.
|
||||
- [x] T022 [US1] Preserve Product Surface budgets: one lifecycle summary, one dominant next action, secondary technical links demoted, and no raw IDs/source keys/provider payloads in customer-facing defaults.
|
||||
|
||||
## Phase 4: User Story 2 - Destructive lifecycle actions are blocked or audited correctly (Priority: P1)
|
||||
|
||||
**Goal**: Hold, unhold, archive, expire, delete, and purge-like behavior is explicitly scoped, authorized, confirmation-backed when visible, audited, and blocked when held.
|
||||
|
||||
**Independent Test**: A held artifact cannot be deleted through UI, direct action, or retention cleanup; an allowed lifecycle action records audit proof and leaves no accessible orphan file.
|
||||
|
||||
### Tests for User Story 2
|
||||
|
||||
- [ ] T023 [P] [US2] For families classified `SUPPORTED_NOW` for hold, add failing tests proving held artifacts cannot be deleted, hard-deleted, or pruned; if review packs, stored reports, or any other high-risk family is classified `DEFERRED` or `PRODUCT_DECISION_REQUIRED`, record the no-runtime-mutation rationale instead of fabricating held fixtures.
|
||||
- [ ] T024 [P] [US2] Add failing direct-execution authorization tests for delete/archive/expire/hold/unhold actions, including allowed actor, missing capability, wrong workspace, wrong managed environment, and customer reviewer.
|
||||
- [x] T025 [P] [US2] Add failing Filament action tests for destructive/high-impact lifecycle actions proving `requiresConfirmation`, disabled/hidden state, and server-side denial.
|
||||
- [x] T026 [P] [US2] Add failing audit tests proving lifecycle actions record actor, workspace, managed environment, artifact family, safe artifact reference, old state, new state, result, and failure reason.
|
||||
|
||||
### Implementation for User Story 2
|
||||
|
||||
- [ ] T027 [US2] Add current-table lifecycle/hold/delete metadata migrations only where the lifecycle matrix classifies the behavior `SUPPORTED_NOW`, proves a current-release need, and no existing field can carry the behavior safely.
|
||||
- [x] T028 [US2] Implement bounded lifecycle transition services/actions on existing artifact owners; do not create a generic artifact registry or workflow engine.
|
||||
- [x] T029 [US2] Update Filament lifecycle actions only on existing artifact owner surfaces, using `Action::make(...)->action(...)`, `->requiresConfirmation()`, policy/gate authorization, and audit proof.
|
||||
- [ ] T030 [US2] Update retention/prune commands so held artifacts are skipped for `SUPPORTED_NOW` hold families, delete behavior is explicit, deferred families preserve current behavior, and failures do not mark artifacts as safely deleted when file/database work failed.
|
||||
- [x] T031 [US2] If irreversible purge or export-before-delete becomes necessary, stop and record `follow-up-spec` instead of implementing it inside Spec 406.
|
||||
|
||||
## Phase 5: User Story 3 - Customer-safe exports and downloads remain bounded (Priority: P1)
|
||||
|
||||
**Goal**: Released customer-safe artifacts can be downloaded/exported only when valid and authorized; unreleased/internal/deleted/missing-file artifacts remain unavailable.
|
||||
|
||||
**Independent Test**: Customer reviewer can download a released customer-safe artifact and cannot access unreleased, internal, failed, deleted, expired-without-access, or missing-file artifacts.
|
||||
|
||||
### Tests for User Story 3
|
||||
|
||||
- [x] T032 [P] [US3] Add or update `ReviewPackDownloadController` tests for authorized, missing-capability, wrong-workspace, wrong-environment, customer reviewer, expired, deleted/blocked, failed, and missing-file cases.
|
||||
- [ ] T033 [P] [US3] Add or update management-PDF download tests for `StoredReport` status/file/customer-output gate behavior and invalid file states.
|
||||
- [ ] T034 [P] [US3] Add or update customer-safe output tests proving exports/downloads exclude internal-only evidence, raw provider payloads, raw source keys, OperationRun internals, stack traces, internal exception messages, system-only links, and cross-workspace data.
|
||||
- [x] T035 [P] [US3] Add signed-url/current-state regression tests proving an old signed URL re-checks current artifact lifecycle and file state before returning bytes.
|
||||
|
||||
### Implementation for User Story 3
|
||||
|
||||
- [x] T036 [US3] Harden review-pack and management-PDF download controllers so lifecycle state, customer-output gate, authorization, file existence, file size, disk/path, and hash expectations are re-checked at request time.
|
||||
- [ ] T037 [US3] Harden export/download builders so customer-safe output is derived from released/customer-safe content only and raw/internal proof remains technical or support-gated.
|
||||
- [x] T038 [US3] Ensure deleted/failed/missing-file artifacts return safe denial or not-found responses and never stream partial/internal bytes.
|
||||
- [x] T039 [US3] Record customer-safe export/download proof in `implementation-report.md`.
|
||||
|
||||
## Phase 6: User Story 4 - Retention behavior is deterministic (Priority: P2)
|
||||
|
||||
**Goal**: Retention jobs/actions expire or archive only eligible artifacts, skip held artifacts for `SUPPORTED_NOW` hold families, and report product-decision gaps instead of inventing broad purge behavior.
|
||||
|
||||
**Independent Test**: Retention logic updates only eligible artifacts, remains idempotent, skips held artifacts for `SUPPORTED_NOW` hold families, and records audit/OperationRun proof according to existing conventions.
|
||||
|
||||
### Tests for User Story 4
|
||||
|
||||
- [ ] T040 [P] [US4] Add or update prune/retention command tests for review packs, stored reports, and any other family classified `SUPPORTED_NOW` for retention/hold covering eligible, not-yet-eligible, held where applicable, wrong-workspace, already-terminal, missing-file, and command retry cases.
|
||||
- [ ] T041 [P] [US4] Add tests for configured retention values and explicit defaults without legal/compliance claims.
|
||||
- [ ] T042 [P] [US4] Add tests proving retention cleanup does not delete core audit trails or OperationRun proof unless a specific existing contract permits it.
|
||||
|
||||
### Implementation for User Story 4
|
||||
|
||||
- [ ] T043 [US4] Update retention commands/jobs only where tests prove lifecycle gaps; keep behavior idempotent and family-local.
|
||||
- [ ] T044 [US4] Add query-backed indexes only if retention scans or hold-state checks require them and document write-overhead risk.
|
||||
- [ ] T045 [US4] Record scheduler, queue, config/env, storage, and Dokploy deployment impact in `implementation-report.md`.
|
||||
|
||||
## Phase 7: Browser Proof And Product Sanity
|
||||
|
||||
**Purpose**: Prove representative rendered behavior and customer-safe boundaries.
|
||||
|
||||
- [x] T046 Run focused browser proof for authorized review-pack detail/download state.
|
||||
- [x] T047 Run focused browser proof for held artifact delete blocked state on an existing owner surface only for families classified `SUPPORTED_NOW` for hold; otherwise record the matrix-backed `N/A` rationale without claiming proof.
|
||||
- [ ] T048 Run focused browser proof for customer-review released artifact access and unreleased/internal artifact denial.
|
||||
- [ ] T049 Run focused browser proof for missing-file or deleted/expired artifact not being offered as valid download.
|
||||
- [x] T050 Record route/surface, actor/role, workspace/environment, artifact type, lifecycle state, expected result, actual result, console/runtime/network result, and screenshot/artifact path where relevant.
|
||||
- [x] T051 Complete Human Product Sanity and record purpose clarity, one dominant next action, technical detail demotion, canonical status labels, visible complexity outcome, and trust result.
|
||||
- [x] T052 Review `docs/ui-ux-enterprise-audit/route-inventory.md` and `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`; update them if rendered surface scope materially changed, or record a checked no-update rationale in `implementation-report.md`.
|
||||
|
||||
## Phase 8: Final Validation And Close-Out
|
||||
|
||||
**Purpose**: Confirm the package is ready for review and no unrelated work entered the slice.
|
||||
|
||||
- [x] T053 Run `git diff --check` from repo root and record result.
|
||||
- [x] T054 Run `cd apps/platform && ./vendor/bin/sail pint --dirty` or repo-equivalent formatting for changed PHP files.
|
||||
- [x] T055 Run focused Spec406 test command, e.g. `cd apps/platform && ./vendor/bin/sail artisan test --compact --filter=Spec406`, and record result.
|
||||
- [x] T056 Run targeted existing family tests for ReviewPack, StoredReport/management PDF, Evidence, CustomerReviewWorkspace, OperationRun, and Findings touched by the implementation.
|
||||
- [x] T057 Run PostgreSQL lane if migrations/indexes/constraints are added, and record exact command/result.
|
||||
- [x] T058 Run focused browser proof and record exact command/result, or exact blocker without claiming proof.
|
||||
- [x] T059 Verify reports, logs, screenshots, generated artifacts, and fixtures do not include secrets, tokens, raw credential payloads, sensitive provider payloads, customer data, private URLs, or stack traces.
|
||||
- [x] T060 Complete all implementation-report sections, including lifecycle matrix, Spec 404/405 condition carry-forward assessment, per-family hold/delete support classification, runtime changes, migrations, tests, browser proof, authorization/customer-safe proof, file/database consistency, retention/hold/delete proof, findings, deferred items, validation commands, and next step.
|
||||
- [x] T061 Set final Spec 406 gate result to `PASS`, `PASS WITH CONDITIONS`, or `FAIL` according to remaining P0/P1 lifecycle risk; do not set `PASS` when a required hold/delete or Spec 404/405 carry-forward condition remains unresolved for a high-risk touched path.
|
||||
- [x] T062 Confirm the final response states Livewire v4 compliance, provider registration location, global search posture, destructive/high-impact action posture, asset strategy, tests/browser result, deployment impact, visible complexity outcome, no completed-spec rewrite assertion, and explicit application implementation status.
|
||||
|
||||
## Non-Goals Checklist
|
||||
|
||||
- [x] NT001 Do not add a new customer portal, artifact portal, export center, panel, navigation entry, or broad product module.
|
||||
- [x] NT002 Do not introduce legal compliance claims such as GDPR-compliant retention, legally defensible deletion, audit-certified archive, or regulatory-grade lifecycle.
|
||||
- [x] NT003 Do not create a generic artifact registry table, universal lifecycle framework, purge platform, or workflow engine.
|
||||
- [x] NT004 Do not rewrite evidence/currentness semantics from Spec 403, PDF runtime behavior from Spec 404, JSONB storage behavior from Spec 405, or read-only lifecycle close-out from Spec 267.
|
||||
- [x] NT005 Do not change Graph/provider integration, backup/restore semantics, authorization model, global search posture, or panel/provider registration unless this spec is updated.
|
||||
- [x] NT006 Do not remove, uncheck, normalize, or rewrite completed historical specs or implementation reports.
|
||||
- [x] NT007 Do not claim Spec 404/405 staging, production, PDF-runtime, storage, or Dokploy readiness unless proven in Spec 406 or explicitly ruled not applicable in the implementation report.
|
||||
|
||||
## Dependencies And Execution Order
|
||||
|
||||
- Phase 1 and Phase 2 must complete before runtime edits.
|
||||
- User Stories 1, 2, and 3 are P1 and should all pass before a full `PASS` gate.
|
||||
- User Story 4 can be `PASS WITH CONDITIONS` only when residual retention decisions are safe, documented, and not P0/P1 for high-risk artifacts.
|
||||
- Browser proof and Human Product Sanity must complete before close-out when rendered behavior changed.
|
||||
|
||||
## Parallel Execution Examples
|
||||
|
||||
- T007 through T012 can run in parallel by artifact family.
|
||||
- T016 through T019 can run in parallel by test family after matrix rows are drafted.
|
||||
- T023 through T026 can run in parallel by action/audit/authorization concern.
|
||||
- T032 through T035 can run in parallel by controller/customer-safe path.
|
||||
|
||||
## Recommended Implementation Strategy
|
||||
|
||||
Start with the lifecycle matrix and only implement defects that are classified as P0/P1 or required to satisfy high-risk artifact proof. Keep hold/delete/export behavior family-local and current-owner based. If the implementation discovers an irreversible purge, export-before-delete, or customer-portal requirement, split it into a follow-up instead of widening Spec 406.
|
||||
Loading…
Reference in New Issue
Block a user