feat: 267-artifact-lifecycle-retention → platform-dev (#323)
Automated PR to merge `267-artifact-lifecycle-retention` into `platform-dev`. Created by Copilot. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #323
This commit is contained in:
parent
3aeb0d04b8
commit
6bf8e7f76b
@ -18,7 +18,9 @@ final class BadgeCatalog
|
|||||||
BadgeDomain::GovernanceArtifactExistence->value => Domains\GovernanceArtifactExistenceBadge::class,
|
BadgeDomain::GovernanceArtifactExistence->value => Domains\GovernanceArtifactExistenceBadge::class,
|
||||||
BadgeDomain::GovernanceArtifactContent->value => Domains\GovernanceArtifactContentBadge::class,
|
BadgeDomain::GovernanceArtifactContent->value => Domains\GovernanceArtifactContentBadge::class,
|
||||||
BadgeDomain::GovernanceArtifactFreshness->value => Domains\GovernanceArtifactFreshnessBadge::class,
|
BadgeDomain::GovernanceArtifactFreshness->value => Domains\GovernanceArtifactFreshnessBadge::class,
|
||||||
|
BadgeDomain::GovernanceArtifactLifecycle->value => Domains\GovernanceArtifactLifecycleBadge::class,
|
||||||
BadgeDomain::GovernanceArtifactPublicationReadiness->value => Domains\GovernanceArtifactPublicationReadinessBadge::class,
|
BadgeDomain::GovernanceArtifactPublicationReadiness->value => Domains\GovernanceArtifactPublicationReadinessBadge::class,
|
||||||
|
BadgeDomain::GovernanceArtifactRetention->value => Domains\GovernanceArtifactRetentionBadge::class,
|
||||||
BadgeDomain::GovernanceArtifactActionability->value => Domains\GovernanceArtifactActionabilityBadge::class,
|
BadgeDomain::GovernanceArtifactActionability->value => Domains\GovernanceArtifactActionabilityBadge::class,
|
||||||
BadgeDomain::OperatorExplanationEvaluationResult->value => Domains\OperatorExplanationEvaluationResultBadge::class,
|
BadgeDomain::OperatorExplanationEvaluationResult->value => Domains\OperatorExplanationEvaluationResultBadge::class,
|
||||||
BadgeDomain::OperatorExplanationTrustworthiness->value => Domains\OperatorExplanationTrustworthinessBadge::class,
|
BadgeDomain::OperatorExplanationTrustworthiness->value => Domains\OperatorExplanationTrustworthinessBadge::class,
|
||||||
|
|||||||
@ -9,7 +9,9 @@ enum BadgeDomain: string
|
|||||||
case GovernanceArtifactExistence = 'governance_artifact_existence';
|
case GovernanceArtifactExistence = 'governance_artifact_existence';
|
||||||
case GovernanceArtifactContent = 'governance_artifact_content';
|
case GovernanceArtifactContent = 'governance_artifact_content';
|
||||||
case GovernanceArtifactFreshness = 'governance_artifact_freshness';
|
case GovernanceArtifactFreshness = 'governance_artifact_freshness';
|
||||||
|
case GovernanceArtifactLifecycle = 'governance_artifact_lifecycle';
|
||||||
case GovernanceArtifactPublicationReadiness = 'governance_artifact_publication_readiness';
|
case GovernanceArtifactPublicationReadiness = 'governance_artifact_publication_readiness';
|
||||||
|
case GovernanceArtifactRetention = 'governance_artifact_retention';
|
||||||
case GovernanceArtifactActionability = 'governance_artifact_actionability';
|
case GovernanceArtifactActionability = 'governance_artifact_actionability';
|
||||||
case OperatorExplanationEvaluationResult = 'operator_explanation_evaluation_result';
|
case OperatorExplanationEvaluationResult = 'operator_explanation_evaluation_result';
|
||||||
case OperatorExplanationTrustworthiness = 'operator_explanation_trustworthiness';
|
case OperatorExplanationTrustworthiness = 'operator_explanation_trustworthiness';
|
||||||
|
|||||||
@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class GovernanceArtifactLifecycleBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
return match (BadgeCatalog::normalizeState($value)) {
|
||||||
|
'current' => new BadgeSpec('Current', 'success', 'heroicon-m-check-circle'),
|
||||||
|
'historical' => new BadgeSpec('Historical', 'gray', 'heroicon-m-archive-box'),
|
||||||
|
'superseded' => new BadgeSpec('Superseded', 'warning', 'heroicon-m-arrow-path'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Support\Badges\Domains;
|
||||||
|
|
||||||
|
use App\Support\Badges\BadgeCatalog;
|
||||||
|
use App\Support\Badges\BadgeMapper;
|
||||||
|
use App\Support\Badges\BadgeSpec;
|
||||||
|
|
||||||
|
final class GovernanceArtifactRetentionBadge implements BadgeMapper
|
||||||
|
{
|
||||||
|
public function spec(mixed $value): BadgeSpec
|
||||||
|
{
|
||||||
|
return match (BadgeCatalog::normalizeState($value)) {
|
||||||
|
'retained' => new BadgeSpec('Retained', 'info', 'heroicon-m-archive-box'),
|
||||||
|
'hold' => new BadgeSpec('On hold', 'warning', 'heroicon-m-lock-closed'),
|
||||||
|
'deletion_requested' => new BadgeSpec('Deletion requested', 'danger', 'heroicon-m-trash'),
|
||||||
|
'expired_direct_access' => new BadgeSpec('Direct access expired', 'gray', 'heroicon-m-clock'),
|
||||||
|
default => BadgeSpec::unknown(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,6 +34,10 @@ public function __construct(
|
|||||||
public array $dimensions = [],
|
public array $dimensions = [],
|
||||||
public ?ArtifactTruthCause $reason = null,
|
public ?ArtifactTruthCause $reason = null,
|
||||||
public ?OperatorExplanationPattern $operatorExplanation = null,
|
public ?OperatorExplanationPattern $operatorExplanation = null,
|
||||||
|
public ?string $displayReference = null,
|
||||||
|
public ?string $integrityAnchor = null,
|
||||||
|
public ?string $lifecycleState = null,
|
||||||
|
public ?string $retentionState = null,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function primaryDimension(): ?ArtifactTruthDimension
|
public function primaryDimension(): ?ArtifactTruthDimension
|
||||||
@ -87,6 +91,10 @@ public function nextStepText(): string
|
|||||||
* nextActionUrl: ?string,
|
* nextActionUrl: ?string,
|
||||||
* relatedRunId: ?int,
|
* relatedRunId: ?int,
|
||||||
* relatedArtifactUrl: ?string,
|
* relatedArtifactUrl: ?string,
|
||||||
|
* displayReference: ?string,
|
||||||
|
* integrityAnchor: ?string,
|
||||||
|
* lifecycleState: ?string,
|
||||||
|
* retentionState: ?string,
|
||||||
* dimensions: array<int, array{
|
* dimensions: array<int, array{
|
||||||
* axis: string,
|
* axis: string,
|
||||||
* state: string,
|
* state: string,
|
||||||
@ -154,6 +162,10 @@ public function toArray(?CompressedGovernanceOutcome $compressedOutcome = null):
|
|||||||
'nextActionUrl' => $this->nextActionUrl,
|
'nextActionUrl' => $this->nextActionUrl,
|
||||||
'relatedRunId' => $this->relatedRunId,
|
'relatedRunId' => $this->relatedRunId,
|
||||||
'relatedArtifactUrl' => $this->relatedArtifactUrl,
|
'relatedArtifactUrl' => $this->relatedArtifactUrl,
|
||||||
|
'displayReference' => $this->displayReference,
|
||||||
|
'integrityAnchor' => $this->integrityAnchor,
|
||||||
|
'lifecycleState' => $this->lifecycleState,
|
||||||
|
'retentionState' => $this->retentionState,
|
||||||
'dimensions' => array_values(array_map(
|
'dimensions' => array_values(array_map(
|
||||||
static fn (ArtifactTruthDimension $dimension): array => $dimension->toArray(),
|
static fn (ArtifactTruthDimension $dimension): array => $dimension->toArray(),
|
||||||
array_filter(
|
array_filter(
|
||||||
|
|||||||
@ -10,8 +10,10 @@
|
|||||||
use App\Filament\Resources\TenantReviewResource;
|
use App\Filament\Resources\TenantReviewResource;
|
||||||
use App\Models\BaselineSnapshot;
|
use App\Models\BaselineSnapshot;
|
||||||
use App\Models\EvidenceSnapshot;
|
use App\Models\EvidenceSnapshot;
|
||||||
|
use App\Models\FindingExceptionDecision;
|
||||||
use App\Models\OperationRun;
|
use App\Models\OperationRun;
|
||||||
use App\Models\ReviewPack;
|
use App\Models\ReviewPack;
|
||||||
|
use App\Models\StoredReport;
|
||||||
use App\Models\TenantReview;
|
use App\Models\TenantReview;
|
||||||
use App\Services\Baselines\BaselineSnapshotTruthResolver;
|
use App\Services\Baselines\BaselineSnapshotTruthResolver;
|
||||||
use App\Services\Baselines\SnapshotRendering\FidelityState;
|
use App\Services\Baselines\SnapshotRendering\FidelityState;
|
||||||
@ -36,6 +38,7 @@
|
|||||||
use Filament\Facades\Filament;
|
use Filament\Facades\Filament;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
final class ArtifactTruthPresenter
|
final class ArtifactTruthPresenter
|
||||||
{
|
{
|
||||||
@ -53,6 +56,8 @@ public function for(mixed $record): ?ArtifactTruthEnvelope
|
|||||||
$record instanceof EvidenceSnapshot => $this->forEvidenceSnapshot($record),
|
$record instanceof EvidenceSnapshot => $this->forEvidenceSnapshot($record),
|
||||||
$record instanceof TenantReview => $this->forTenantReview($record),
|
$record instanceof TenantReview => $this->forTenantReview($record),
|
||||||
$record instanceof ReviewPack => $this->forReviewPack($record),
|
$record instanceof ReviewPack => $this->forReviewPack($record),
|
||||||
|
$record instanceof StoredReport => $this->forStoredReport($record),
|
||||||
|
$record instanceof FindingExceptionDecision => $this->forFindingExceptionDecision($record),
|
||||||
$record instanceof OperationRun => $this->forOperationRun($record),
|
$record instanceof OperationRun => $this->forOperationRun($record),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@ -65,6 +70,8 @@ public function forFresh(mixed $record): ?ArtifactTruthEnvelope
|
|||||||
$record instanceof EvidenceSnapshot => $this->forEvidenceSnapshotFresh($record),
|
$record instanceof EvidenceSnapshot => $this->forEvidenceSnapshotFresh($record),
|
||||||
$record instanceof TenantReview => $this->forTenantReviewFresh($record),
|
$record instanceof TenantReview => $this->forTenantReviewFresh($record),
|
||||||
$record instanceof ReviewPack => $this->forReviewPackFresh($record),
|
$record instanceof ReviewPack => $this->forReviewPackFresh($record),
|
||||||
|
$record instanceof StoredReport => $this->forStoredReportFresh($record),
|
||||||
|
$record instanceof FindingExceptionDecision => $this->forFindingExceptionDecisionFresh($record),
|
||||||
$record instanceof OperationRun => $this->forOperationRunFresh($record),
|
$record instanceof OperationRun => $this->forOperationRunFresh($record),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
@ -289,6 +296,9 @@ private function buildBaselineSnapshotEnvelope(BaselineSnapshot $snapshot): Arti
|
|||||||
qualifier: (int) (Arr::get($summary, 'gaps.count', 0)) > 0 ? 'review needed' : null,
|
qualifier: (int) (Arr::get($summary, 'gaps.count', 0)) > 0 ? 'review needed' : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
displayReference: $this->artifactDisplayReference('Baseline snapshot', $snapshot),
|
||||||
|
lifecycleState: $isHistorical ? 'superseded' : 'current',
|
||||||
|
retentionState: 'retained',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -452,6 +462,10 @@ private function buildEvidenceSnapshotEnvelope(EvidenceSnapshot $snapshot): Arti
|
|||||||
qualifier: $staleDimensions > 0 ? 'refresh recommended' : null,
|
qualifier: $staleDimensions > 0 ? 'refresh recommended' : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
displayReference: $this->artifactDisplayReference('Evidence snapshot', $snapshot),
|
||||||
|
integrityAnchor: $snapshot->fingerprint,
|
||||||
|
lifecycleState: $this->evidenceLifecycleState($snapshot, $artifactExistence),
|
||||||
|
retentionState: $this->evidenceRetentionState($snapshot),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -570,6 +584,20 @@ private function buildTenantReviewEnvelope(TenantReview $review): ArtifactTruthE
|
|||||||
? OperationRunLinks::tenantlessView((int) $review->operation_run_id)
|
? OperationRunLinks::tenantlessView((int) $review->operation_run_id)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
$displayReference = $this->artifactDisplayReference('Review', $review);
|
||||||
|
$integrityAnchor = is_string($review->fingerprint) && trim($review->fingerprint) !== ''
|
||||||
|
? $review->fingerprint
|
||||||
|
: null;
|
||||||
|
$lifecycleState = $artifactExistence === 'historical_only' ? 'historical' : 'current';
|
||||||
|
$retentionState = 'retained';
|
||||||
|
|
||||||
|
if ($review->currentExportReviewPack instanceof ReviewPack) {
|
||||||
|
$displayReference = $this->artifactDisplayReference('Current export', $review->currentExportReviewPack);
|
||||||
|
$integrityAnchor = $review->currentExportReviewPack->sha256 ?: $review->currentExportReviewPack->fingerprint;
|
||||||
|
$lifecycleState = $this->reviewPackLifecycleState($review->currentExportReviewPack);
|
||||||
|
$retentionState = $this->reviewPackRetentionState($review->currentExportReviewPack);
|
||||||
|
}
|
||||||
|
|
||||||
if ($publishBlockers !== [] && $review->tenant !== null) {
|
if ($publishBlockers !== [] && $review->tenant !== null) {
|
||||||
$nextActionUrl = $this->panelSafeTenantArtifactUrl(
|
$nextActionUrl = $this->panelSafeTenantArtifactUrl(
|
||||||
fn (): string => TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant)
|
fn (): string => TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $review->tenant)
|
||||||
@ -636,6 +664,10 @@ private function buildTenantReviewEnvelope(TenantReview $review): ArtifactTruthE
|
|||||||
qualifier: $publishBlockers !== [] ? 'resolve before publish' : null,
|
qualifier: $publishBlockers !== [] ? 'resolve before publish' : null,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
displayReference: $displayReference,
|
||||||
|
integrityAnchor: $integrityAnchor,
|
||||||
|
lifecycleState: $lifecycleState,
|
||||||
|
retentionState: $retentionState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -819,6 +851,134 @@ private function buildReviewPackEnvelope(ReviewPack $pack): ArtifactTruthEnvelop
|
|||||||
visibilityTier: CountDescriptor::VISIBILITY_DIAGNOSTIC,
|
visibilityTier: CountDescriptor::VISIBILITY_DIAGNOSTIC,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
displayReference: $this->artifactDisplayReference('Review pack', $pack),
|
||||||
|
integrityAnchor: $pack->sha256 ?: $pack->fingerprint,
|
||||||
|
lifecycleState: $this->reviewPackLifecycleState($pack),
|
||||||
|
retentionState: $this->reviewPackRetentionState($pack),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forStoredReport(StoredReport $report): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
return $this->resolveEnvelope(
|
||||||
|
record: $report,
|
||||||
|
variant: 'stored_report',
|
||||||
|
resolver: fn (): ArtifactTruthEnvelope => $this->buildStoredReportEnvelope($report),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forStoredReportFresh(StoredReport $report): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
return $this->resolveEnvelope(
|
||||||
|
record: $report,
|
||||||
|
variant: 'stored_report',
|
||||||
|
resolver: fn (): ArtifactTruthEnvelope => $this->buildStoredReportEnvelope($report),
|
||||||
|
fresh: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildStoredReportEnvelope(StoredReport $report): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
$report->loadMissing('tenant');
|
||||||
|
|
||||||
|
$latestReport = StoredReport::query()
|
||||||
|
->where('tenant_id', (int) $report->tenant_id)
|
||||||
|
->where('report_type', $report->report_type)
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$isCurrent = $latestReport?->is($report) ?? true;
|
||||||
|
$artifactExistence = $isCurrent ? 'created' : 'historical_only';
|
||||||
|
$freshnessState = $isCurrent ? 'current' : 'stale';
|
||||||
|
$lifecycleState = $isCurrent ? 'current' : 'historical';
|
||||||
|
|
||||||
|
return $this->makeEnvelope(
|
||||||
|
artifactFamily: 'stored_report',
|
||||||
|
artifactKey: 'stored_report:'.$report->getKey(),
|
||||||
|
workspaceId: (int) $report->workspace_id,
|
||||||
|
tenantId: (int) $report->tenant_id,
|
||||||
|
executionOutcome: null,
|
||||||
|
artifactExistence: $artifactExistence,
|
||||||
|
contentState: 'trusted',
|
||||||
|
freshnessState: $freshnessState,
|
||||||
|
publicationReadiness: null,
|
||||||
|
supportState: 'normal',
|
||||||
|
actionability: 'none',
|
||||||
|
primaryDomain: BadgeDomain::GovernanceArtifactLifecycle,
|
||||||
|
primaryState: $lifecycleState,
|
||||||
|
primaryExplanation: $isCurrent
|
||||||
|
? 'This report is the latest retained record for its report type.'
|
||||||
|
: 'This report remains readable as retained history, but a newer report is the current record.',
|
||||||
|
diagnosticLabel: null,
|
||||||
|
reason: null,
|
||||||
|
nextActionLabel: 'No action needed',
|
||||||
|
nextActionUrl: null,
|
||||||
|
relatedRunId: null,
|
||||||
|
relatedArtifactUrl: null,
|
||||||
|
includePublicationDimension: false,
|
||||||
|
displayReference: sprintf('Stored report #%d (%s)', $report->getKey(), $this->storedReportReferenceLabel($report)),
|
||||||
|
integrityAnchor: $report->fingerprint,
|
||||||
|
lifecycleState: $lifecycleState,
|
||||||
|
retentionState: 'retained',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forFindingExceptionDecision(FindingExceptionDecision $decision): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
return $this->resolveEnvelope(
|
||||||
|
record: $decision,
|
||||||
|
variant: 'finding_exception_decision',
|
||||||
|
resolver: fn (): ArtifactTruthEnvelope => $this->buildFindingExceptionDecisionEnvelope($decision),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function forFindingExceptionDecisionFresh(FindingExceptionDecision $decision): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
return $this->resolveEnvelope(
|
||||||
|
record: $decision,
|
||||||
|
variant: 'finding_exception_decision',
|
||||||
|
resolver: fn (): ArtifactTruthEnvelope => $this->buildFindingExceptionDecisionEnvelope($decision),
|
||||||
|
fresh: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildFindingExceptionDecisionEnvelope(FindingExceptionDecision $decision): ArtifactTruthEnvelope
|
||||||
|
{
|
||||||
|
$decision->loadMissing('exception.currentDecision');
|
||||||
|
|
||||||
|
$isCurrent = $decision->exception?->current_decision_id === (int) $decision->getKey();
|
||||||
|
$artifactExistence = $isCurrent ? 'created' : 'historical_only';
|
||||||
|
$lifecycleState = $isCurrent ? 'current' : 'superseded';
|
||||||
|
|
||||||
|
return $this->makeEnvelope(
|
||||||
|
artifactFamily: 'finding_exception_decision',
|
||||||
|
artifactKey: 'finding_exception_decision:'.$decision->getKey(),
|
||||||
|
workspaceId: (int) $decision->workspace_id,
|
||||||
|
tenantId: $decision->tenant_id !== null ? (int) $decision->tenant_id : null,
|
||||||
|
executionOutcome: null,
|
||||||
|
artifactExistence: $artifactExistence,
|
||||||
|
contentState: 'trusted',
|
||||||
|
freshnessState: $isCurrent ? 'current' : 'stale',
|
||||||
|
publicationReadiness: null,
|
||||||
|
supportState: 'normal',
|
||||||
|
actionability: 'none',
|
||||||
|
primaryDomain: BadgeDomain::GovernanceArtifactLifecycle,
|
||||||
|
primaryState: $lifecycleState,
|
||||||
|
primaryExplanation: $isCurrent
|
||||||
|
? 'This append-only decision row is the current accepted-risk decision.'
|
||||||
|
: 'This append-only decision row remains in history, but a newer decision is now current.',
|
||||||
|
diagnosticLabel: null,
|
||||||
|
reason: null,
|
||||||
|
nextActionLabel: 'No action needed',
|
||||||
|
nextActionUrl: null,
|
||||||
|
relatedRunId: null,
|
||||||
|
relatedArtifactUrl: null,
|
||||||
|
includePublicationDimension: false,
|
||||||
|
displayReference: $this->artifactDisplayReference('Accepted-risk decision', $decision),
|
||||||
|
integrityAnchor: $decision->decided_at?->toIso8601String(),
|
||||||
|
lifecycleState: $lifecycleState,
|
||||||
|
retentionState: 'retained',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -907,6 +1067,10 @@ private function buildOperationRunEnvelope(OperationRun $run): ArtifactTruthEnve
|
|||||||
$artifactEnvelope->operatorExplanation?->countDescriptors ?? [],
|
$artifactEnvelope->operatorExplanation?->countDescriptors ?? [],
|
||||||
$this->runCountDescriptors($run),
|
$this->runCountDescriptors($run),
|
||||||
),
|
),
|
||||||
|
displayReference: $artifactEnvelope->displayReference,
|
||||||
|
integrityAnchor: $artifactEnvelope->integrityAnchor,
|
||||||
|
lifecycleState: $artifactEnvelope->lifecycleState,
|
||||||
|
retentionState: $artifactEnvelope->retentionState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1070,15 +1234,38 @@ private function makeEnvelope(
|
|||||||
?string $relatedArtifactUrl,
|
?string $relatedArtifactUrl,
|
||||||
bool $includePublicationDimension,
|
bool $includePublicationDimension,
|
||||||
array $countDescriptors = [],
|
array $countDescriptors = [],
|
||||||
|
?string $displayReference = null,
|
||||||
|
?string $integrityAnchor = null,
|
||||||
|
?string $lifecycleState = null,
|
||||||
|
?string $retentionState = null,
|
||||||
): ArtifactTruthEnvelope {
|
): ArtifactTruthEnvelope {
|
||||||
$primarySpec = BadgeCatalog::spec($primaryDomain, $primaryState);
|
$primarySpec = BadgeCatalog::spec($primaryDomain, $primaryState);
|
||||||
$dimensions = [
|
$dimensions = [
|
||||||
$this->dimension(BadgeDomain::GovernanceArtifactExistence, $artifactExistence, 'artifact_existence', $primaryDomain === BadgeDomain::GovernanceArtifactExistence ? 'primary' : 'diagnostic'),
|
$this->dimension(BadgeDomain::GovernanceArtifactExistence, $artifactExistence, 'artifact_existence', $primaryDomain === BadgeDomain::GovernanceArtifactExistence ? 'primary' : 'diagnostic'),
|
||||||
$this->dimension(BadgeDomain::GovernanceArtifactContent, $contentState, 'content_fidelity', $primaryDomain === BadgeDomain::GovernanceArtifactContent ? 'primary' : 'diagnostic'),
|
$this->dimension(BadgeDomain::GovernanceArtifactContent, $contentState, 'content_fidelity', $primaryDomain === BadgeDomain::GovernanceArtifactContent ? 'primary' : 'diagnostic'),
|
||||||
$this->dimension(BadgeDomain::GovernanceArtifactFreshness, $freshnessState, 'data_freshness', $primaryDomain === BadgeDomain::GovernanceArtifactFreshness ? 'primary' : 'diagnostic'),
|
$this->dimension(BadgeDomain::GovernanceArtifactFreshness, $freshnessState, 'data_freshness', $primaryDomain === BadgeDomain::GovernanceArtifactFreshness ? 'primary' : 'diagnostic'),
|
||||||
$this->dimension(BadgeDomain::GovernanceArtifactActionability, $actionability, 'operator_actionability', 'diagnostic'),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($lifecycleState !== null) {
|
||||||
|
$dimensions[] = $this->dimension(
|
||||||
|
BadgeDomain::GovernanceArtifactLifecycle,
|
||||||
|
$lifecycleState,
|
||||||
|
'artifact_lifecycle',
|
||||||
|
$primaryDomain === BadgeDomain::GovernanceArtifactLifecycle ? 'primary' : 'diagnostic',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($retentionState !== null) {
|
||||||
|
$dimensions[] = $this->dimension(
|
||||||
|
BadgeDomain::GovernanceArtifactRetention,
|
||||||
|
$retentionState,
|
||||||
|
'retention_state',
|
||||||
|
$primaryDomain === BadgeDomain::GovernanceArtifactRetention ? 'primary' : 'diagnostic',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dimensions[] = $this->dimension(BadgeDomain::GovernanceArtifactActionability, $actionability, 'operator_actionability', 'diagnostic');
|
||||||
|
|
||||||
if ($includePublicationDimension && $publicationReadiness !== null) {
|
if ($includePublicationDimension && $publicationReadiness !== null) {
|
||||||
$dimensions[] = $this->dimension(
|
$dimensions[] = $this->dimension(
|
||||||
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
BadgeDomain::GovernanceArtifactPublicationReadiness,
|
||||||
@ -1124,6 +1311,10 @@ classification: 'diagnostic',
|
|||||||
relatedArtifactUrl: $relatedArtifactUrl,
|
relatedArtifactUrl: $relatedArtifactUrl,
|
||||||
dimensions: array_values($dimensions),
|
dimensions: array_values($dimensions),
|
||||||
reason: $reason,
|
reason: $reason,
|
||||||
|
displayReference: $displayReference,
|
||||||
|
integrityAnchor: $integrityAnchor,
|
||||||
|
lifecycleState: $lifecycleState,
|
||||||
|
retentionState: $retentionState,
|
||||||
);
|
);
|
||||||
|
|
||||||
return new ArtifactTruthEnvelope(
|
return new ArtifactTruthEnvelope(
|
||||||
@ -1148,6 +1339,10 @@ classification: 'diagnostic',
|
|||||||
dimensions: $draftEnvelope->dimensions,
|
dimensions: $draftEnvelope->dimensions,
|
||||||
reason: $draftEnvelope->reason,
|
reason: $draftEnvelope->reason,
|
||||||
operatorExplanation: $this->operatorExplanationBuilder->fromArtifactTruthEnvelope($draftEnvelope, $countDescriptors),
|
operatorExplanation: $this->operatorExplanationBuilder->fromArtifactTruthEnvelope($draftEnvelope, $countDescriptors),
|
||||||
|
displayReference: $draftEnvelope->displayReference,
|
||||||
|
integrityAnchor: $draftEnvelope->integrityAnchor,
|
||||||
|
lifecycleState: $draftEnvelope->lifecycleState,
|
||||||
|
retentionState: $draftEnvelope->retentionState,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1273,6 +1468,14 @@ private function secondaryFacts(
|
|||||||
state: $truth->freshnessState,
|
state: $truth->freshnessState,
|
||||||
skipWhenState: 'current',
|
skipWhenState: 'current',
|
||||||
),
|
),
|
||||||
|
'artifact_lifecycle' => $truth->lifecycleState !== null
|
||||||
|
? $this->secondaryDimensionFact(
|
||||||
|
label: 'Lifecycle',
|
||||||
|
domain: BadgeDomain::GovernanceArtifactLifecycle,
|
||||||
|
state: $truth->lifecycleState,
|
||||||
|
skipWhenState: 'current',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
'publication_readiness' => $truth->publicationReadiness !== null
|
'publication_readiness' => $truth->publicationReadiness !== null
|
||||||
? $this->secondaryDimensionFact(
|
? $this->secondaryDimensionFact(
|
||||||
label: 'Publication',
|
label: 'Publication',
|
||||||
@ -1281,6 +1484,14 @@ private function secondaryFacts(
|
|||||||
skipWhenState: 'publishable',
|
skipWhenState: 'publishable',
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
'retention_state' => $truth->retentionState !== null
|
||||||
|
? $this->secondaryDimensionFact(
|
||||||
|
label: 'Retention',
|
||||||
|
domain: BadgeDomain::GovernanceArtifactRetention,
|
||||||
|
state: $truth->retentionState,
|
||||||
|
skipWhenState: 'retained',
|
||||||
|
)
|
||||||
|
: null,
|
||||||
'operator_actionability' => $truth->actionability !== 'none'
|
'operator_actionability' => $truth->actionability !== 'none'
|
||||||
? $this->secondaryDimensionFact(
|
? $this->secondaryDimensionFact(
|
||||||
label: 'Actionability',
|
label: 'Actionability',
|
||||||
@ -1309,6 +1520,62 @@ private function secondaryFacts(
|
|||||||
return array_slice($facts, 0, 3);
|
return array_slice($facts, 0, 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function artifactDisplayReference(string $label, Model $record): string
|
||||||
|
{
|
||||||
|
return sprintf('%s #%d', $label, $record->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evidenceLifecycleState(EvidenceSnapshot $snapshot, string $artifactExistence): string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
(string) $snapshot->status === 'superseded' => 'superseded',
|
||||||
|
$artifactExistence === 'historical_only' => 'historical',
|
||||||
|
default => 'current',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function evidenceRetentionState(EvidenceSnapshot $snapshot): string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
(string) $snapshot->status === 'expired' => 'expired_direct_access',
|
||||||
|
$snapshot->expires_at !== null && $snapshot->expires_at->isPast() => 'expired_direct_access',
|
||||||
|
default => 'retained',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reviewPackLifecycleState(ReviewPack $pack): string
|
||||||
|
{
|
||||||
|
if ((string) $pack->status === ReviewPackStatus::Expired->value) {
|
||||||
|
return 'historical';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($pack->tenantReview instanceof TenantReview && $pack->tenantReview->current_export_review_pack_id !== null) {
|
||||||
|
return $pack->tenantReview->current_export_review_pack_id === (int) $pack->getKey()
|
||||||
|
? 'current'
|
||||||
|
: 'superseded';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'current';
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reviewPackRetentionState(ReviewPack $pack): string
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
(string) $pack->status === ReviewPackStatus::Expired->value => 'expired_direct_access',
|
||||||
|
$pack->expires_at !== null && $pack->expires_at->isPast() => 'expired_direct_access',
|
||||||
|
default => 'retained',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function storedReportReferenceLabel(StoredReport $report): string
|
||||||
|
{
|
||||||
|
return Str::of(str_replace('.', ' ', $report->report_type))
|
||||||
|
->replace('_', ' ')
|
||||||
|
->headline()
|
||||||
|
->append(' report')
|
||||||
|
->toString();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{label: string, value: string, badge: ?\App\Support\Badges\BadgeSpec}|null
|
* @return array{label: string, value: string, badge: ?\App\Support\Badges\BadgeSpec}|null
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -76,9 +76,52 @@
|
|||||||
? BadgeCatalog::spec(BadgeDomain::OperatorExplanationTrustworthiness, $operatorExplanation['trustworthinessLevel'])
|
? BadgeCatalog::spec(BadgeDomain::OperatorExplanationTrustworthiness, $operatorExplanation['trustworthinessLevel'])
|
||||||
: null;
|
: null;
|
||||||
$operatorCounts = collect(is_array($operatorExplanation['countDescriptors'] ?? null) ? $operatorExplanation['countDescriptors'] : []);
|
$operatorCounts = collect(is_array($operatorExplanation['countDescriptors'] ?? null) ? $operatorExplanation['countDescriptors'] : []);
|
||||||
|
$displayReference = $normalizeArtifactTruthText($state['displayReference'] ?? null);
|
||||||
|
$lifecycleState = $normalizeArtifactTruthText($state['lifecycleState'] ?? null);
|
||||||
|
$retentionState = $normalizeArtifactTruthText($state['retentionState'] ?? null);
|
||||||
|
|
||||||
|
$badgeSummaryFact = static function (string $label, BadgeDomain $domain, ?string $state) use ($normalizeArtifactTruthText): ?array {
|
||||||
|
$normalizedState = $normalizeArtifactTruthText($state);
|
||||||
|
|
||||||
|
if ($normalizedState === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$spec = BadgeCatalog::spec($domain, $normalizedState);
|
||||||
|
|
||||||
|
if ($spec->label === 'Unknown') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'label' => $label,
|
||||||
|
'value' => $spec->label,
|
||||||
|
'badge' => BadgeCatalog::summaryData($spec),
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
$summaryFacts = collect();
|
$summaryFacts = collect();
|
||||||
|
|
||||||
|
if ($displayReference !== null) {
|
||||||
|
$summaryFacts->push([
|
||||||
|
'label' => 'Artifact reference',
|
||||||
|
'value' => $displayReference,
|
||||||
|
'badge' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lifecycleFact = $badgeSummaryFact('Lifecycle', BadgeDomain::GovernanceArtifactLifecycle, $lifecycleState);
|
||||||
|
|
||||||
|
if (is_array($lifecycleFact)) {
|
||||||
|
$summaryFacts->push($lifecycleFact);
|
||||||
|
}
|
||||||
|
|
||||||
|
$retentionFact = $badgeSummaryFact('Retention', BadgeDomain::GovernanceArtifactRetention, $retentionState);
|
||||||
|
|
||||||
|
if (is_array($retentionFact)) {
|
||||||
|
$summaryFacts->push($retentionFact);
|
||||||
|
}
|
||||||
|
|
||||||
if ($evaluationSpec && $evaluationSpec->label !== 'Unknown') {
|
if ($evaluationSpec && $evaluationSpec->label !== 'Unknown') {
|
||||||
$summaryFacts->push([
|
$summaryFacts->push([
|
||||||
'label' => __('localization.review.result_meaning'),
|
'label' => __('localization.review.result_meaning'),
|
||||||
|
|||||||
@ -281,6 +281,12 @@ function suspendEvidenceSnapshotWorkspace(Tenant $tenant): void
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Outcome summary')
|
->assertSee('Outcome summary')
|
||||||
->assertDontSee('Artifact truth')
|
->assertDontSee('Artifact truth')
|
||||||
|
->assertSee('Artifact reference')
|
||||||
|
->assertSee('Evidence snapshot #'.$snapshot->getKey())
|
||||||
|
->assertSee('Lifecycle')
|
||||||
|
->assertSee('Current')
|
||||||
|
->assertSee('Retention')
|
||||||
|
->assertSee('Retained')
|
||||||
->assertSee('Partially complete')
|
->assertSee('Partially complete')
|
||||||
->assertSee('Refresh evidence before using this snapshot');
|
->assertSee('Refresh evidence before using this snapshot');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -469,6 +469,12 @@ function seedReviewPackEvidence(Tenant $tenant): EvidenceSnapshot
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Outcome summary')
|
->assertSee('Outcome summary')
|
||||||
->assertDontSee('Artifact truth')
|
->assertDontSee('Artifact truth')
|
||||||
|
->assertSee('Artifact reference')
|
||||||
|
->assertSee('Review pack #'.$pack->getKey())
|
||||||
|
->assertSee('Lifecycle')
|
||||||
|
->assertSee('Current')
|
||||||
|
->assertSee('Retention')
|
||||||
|
->assertSee('Retained')
|
||||||
->assertSee('Publishable')
|
->assertSee('Publishable')
|
||||||
->assertSee('#'.$snapshot->getKey())
|
->assertSee('#'.$snapshot->getKey())
|
||||||
->assertSee(OperationRunLinks::view($run, $tenant), false)
|
->assertSee(OperationRunLinks::view($run, $tenant), false)
|
||||||
|
|||||||
@ -197,6 +197,19 @@ function tenantReviewContractHeaderActions(Testable $component): array
|
|||||||
|
|
||||||
setTenantPanelContext($tenant);
|
setTenantPanelContext($tenant);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->get(TenantReviewResource::tenantScopedUrl('view', ['record' => $review], $tenant).'?'.http_build_query([
|
||||||
|
CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1,
|
||||||
|
]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Artifact reference')
|
||||||
|
->assertSee('Current export')
|
||||||
|
->assertSee('#'.$pack->getKey())
|
||||||
|
->assertSee('Lifecycle')
|
||||||
|
->assertSee('Current')
|
||||||
|
->assertSee('Retention')
|
||||||
|
->assertSee('Retained');
|
||||||
|
|
||||||
$component = Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1])
|
$component = Livewire::withQueryParams([CustomerReviewWorkspace::DETAIL_CONTEXT_QUERY_KEY => 1])
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
->test(ViewTenantReview::class, ['record' => $review->getKey()])
|
||||||
@ -244,6 +257,12 @@ function tenantReviewContractHeaderActions(Testable $component): array
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Outcome summary')
|
->assertSee('Outcome summary')
|
||||||
->assertDontSee('Artifact truth')
|
->assertDontSee('Artifact truth')
|
||||||
|
->assertSee('Artifact reference')
|
||||||
|
->assertSee('Review #'.$review->getKey())
|
||||||
|
->assertSee('Lifecycle')
|
||||||
|
->assertSee('Current')
|
||||||
|
->assertSee('Retention')
|
||||||
|
->assertSee('Retained')
|
||||||
->assertSee('Publication blocked')
|
->assertSee('Publication blocked')
|
||||||
->assertSee('Resolve the review blockers before publication');
|
->assertSee('Resolve the review blockers before publication');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\Finding;
|
||||||
|
use App\Models\FindingException;
|
||||||
|
use App\Models\FindingExceptionDecision;
|
||||||
|
use App\Models\ReviewPack;
|
||||||
|
use App\Models\StoredReport;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Support\ReviewPackStatus;
|
||||||
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
it('maps stored reports into immutable reference, lifecycle, and retention truth', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$historical = StoredReport::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
|
||||||
|
'fingerprint' => 'permission-posture-v1',
|
||||||
|
'created_at' => now()->subDay(),
|
||||||
|
'updated_at' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$current = StoredReport::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'report_type' => StoredReport::REPORT_TYPE_PERMISSION_POSTURE,
|
||||||
|
'fingerprint' => 'permission-posture-v2',
|
||||||
|
'previous_fingerprint' => 'permission-posture-v1',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$historicalTruth = app(ArtifactTruthPresenter::class)->for($historical->fresh());
|
||||||
|
$currentTruth = app(ArtifactTruthPresenter::class)->for($current->fresh());
|
||||||
|
|
||||||
|
expect($historicalTruth)->not->toBeNull()
|
||||||
|
->and($currentTruth)->not->toBeNull();
|
||||||
|
|
||||||
|
$historicalState = $historicalTruth?->toArray();
|
||||||
|
$currentState = $currentTruth?->toArray();
|
||||||
|
|
||||||
|
expect($historicalState['displayReference'] ?? null)
|
||||||
|
->toContain('Stored report')
|
||||||
|
->toContain('#'.$historical->getKey())
|
||||||
|
->and($historicalState['integrityAnchor'] ?? null)->toBe('permission-posture-v1')
|
||||||
|
->and($historicalState['lifecycleState'] ?? null)->toBe('historical')
|
||||||
|
->and($historicalState['retentionState'] ?? null)->toBe('retained')
|
||||||
|
->and($currentState['displayReference'] ?? null)->toContain('#'.$current->getKey())
|
||||||
|
->and($currentState['integrityAnchor'] ?? null)->toBe('permission-posture-v2')
|
||||||
|
->and($currentState['lifecycleState'] ?? null)->toBe('current')
|
||||||
|
->and($currentState['retentionState'] ?? null)->toBe('retained');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maps accepted-risk decision history into current and superseded artifact truth', function (): void {
|
||||||
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
$finding = Finding::factory()->for($tenant)->create();
|
||||||
|
|
||||||
|
$exception = FindingException::query()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'finding_id' => (int) $finding->getKey(),
|
||||||
|
'requested_by_user_id' => (int) $user->getKey(),
|
||||||
|
'owner_user_id' => (int) $user->getKey(),
|
||||||
|
'status' => FindingException::STATUS_ACTIVE,
|
||||||
|
'current_validity_state' => FindingException::VALIDITY_VALID,
|
||||||
|
'request_reason' => 'Approve temporary exception',
|
||||||
|
'requested_at' => now()->subDays(10),
|
||||||
|
'approved_at' => now()->subDays(9),
|
||||||
|
'effective_from' => now()->subDays(9),
|
||||||
|
'expires_at' => now()->addDays(14),
|
||||||
|
'review_due_at' => now()->addWeek(),
|
||||||
|
'evidence_summary' => ['reference_count' => 1],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$requestedDecision = $exception->decisions()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'actor_user_id' => (int) $user->getKey(),
|
||||||
|
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
||||||
|
'reason' => 'Approve temporary exception',
|
||||||
|
'metadata' => [],
|
||||||
|
'decided_at' => now()->subDays(10),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$approvedDecision = $exception->decisions()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'actor_user_id' => (int) $user->getKey(),
|
||||||
|
'decision_type' => FindingExceptionDecision::TYPE_APPROVED,
|
||||||
|
'reason' => 'Approved for a bounded review period',
|
||||||
|
'metadata' => [],
|
||||||
|
'effective_from' => now()->subDays(9),
|
||||||
|
'expires_at' => now()->addDays(14),
|
||||||
|
'decided_at' => now()->subDays(9),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$exception->forceFill(['current_decision_id' => (int) $approvedDecision->getKey()])->save();
|
||||||
|
|
||||||
|
$requestedTruth = app(ArtifactTruthPresenter::class)->for($requestedDecision->fresh('exception.currentDecision'));
|
||||||
|
$approvedTruth = app(ArtifactTruthPresenter::class)->for($approvedDecision->fresh('exception.currentDecision'));
|
||||||
|
|
||||||
|
expect($requestedTruth)->not->toBeNull()
|
||||||
|
->and($approvedTruth)->not->toBeNull();
|
||||||
|
|
||||||
|
$requestedState = $requestedTruth?->toArray();
|
||||||
|
$approvedState = $approvedTruth?->toArray();
|
||||||
|
|
||||||
|
expect($requestedState['displayReference'] ?? null)
|
||||||
|
->toContain('Accepted-risk decision')
|
||||||
|
->toContain('#'.$requestedDecision->getKey())
|
||||||
|
->and($requestedState['lifecycleState'] ?? null)->toBe('superseded')
|
||||||
|
->and($requestedState['retentionState'] ?? null)->toBe('retained')
|
||||||
|
->and($approvedState['displayReference'] ?? null)->toContain('#'.$approvedDecision->getKey())
|
||||||
|
->and($approvedState['lifecycleState'] ?? null)->toBe('current')
|
||||||
|
->and($approvedState['retentionState'] ?? null)->toBe('retained');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps expired direct access separate from historical lifecycle on review packs', function (): void {
|
||||||
|
$tenant = Tenant::factory()->create();
|
||||||
|
[$user, $tenant] = createUserWithTenant(tenant: $tenant, role: 'owner');
|
||||||
|
|
||||||
|
$pack = ReviewPack::factory()->create([
|
||||||
|
'tenant_id' => (int) $tenant->getKey(),
|
||||||
|
'workspace_id' => (int) $tenant->workspace_id,
|
||||||
|
'initiated_by_user_id' => (int) $user->getKey(),
|
||||||
|
'status' => ReviewPackStatus::Expired->value,
|
||||||
|
'fingerprint' => 'review-pack-expired',
|
||||||
|
'sha256' => 'sha-expired-pack',
|
||||||
|
'generated_at' => now()->subDays(3),
|
||||||
|
'expires_at' => now()->subDay(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$truth = app(ArtifactTruthPresenter::class)->forReviewPack($pack->fresh());
|
||||||
|
$state = $truth->toArray();
|
||||||
|
|
||||||
|
expect($state['displayReference'] ?? null)->not->toBeNull()
|
||||||
|
->and($state['displayReference'])->toContain('Review pack')
|
||||||
|
->and($state['displayReference'])->toContain('#'.$pack->getKey())
|
||||||
|
->and($state['integrityAnchor'] ?? null)->toBe('sha-expired-pack')
|
||||||
|
->and($state['lifecycleState'] ?? null)->toBe('historical')
|
||||||
|
->and($state['retentionState'] ?? null)->toBe('expired_direct_access');
|
||||||
|
});
|
||||||
@ -1,10 +1,10 @@
|
|||||||
# Spec Candidates
|
# Spec Candidates
|
||||||
|
|
||||||
> **Status:** Active
|
> **Status:** Active
|
||||||
> **Last reviewed:** 2026-05-02
|
> **Last reviewed:** 2026-05-03
|
||||||
> **Use for:** The active repo-based queue of spec candidates that may still need new or refreshed specs
|
> **Use for:** The active repo-based queue of spec candidates that may still need new or refreshed specs
|
||||||
> **Do not use for:** Proof that a candidate is already specced, implemented, or prioritized above the roadmap without repo verification
|
> **Do not use for:** Proof that a candidate is already specced, implemented, or prioritized above the roadmap without repo verification
|
||||||
> **Scoped maintenance:** 2026-05-02 repo-based queue re-audit plus enterprise-SaaS deep-research alignment against current `specs/` truth, including Specs 263 and current-branch 264.
|
> **Scoped maintenance:** 2026-05-03 OperationRun activity feedback candidate intake plus the 2026-05-02 repo-based queue re-audit and enterprise-SaaS deep-research alignment against current `specs/` truth, including Specs 263 and current-branch 264.
|
||||||
>
|
>
|
||||||
> Repo-based next-spec queue for TenantPilot.
|
> Repo-based next-spec queue for TenantPilot.
|
||||||
> This file is not a wishlist. It tracks only open gaps that are still worth turning into new or refreshed specs.
|
> This file is not a wishlist. It tracks only open gaps that are still worth turning into new or refreshed specs.
|
||||||
@ -157,6 +157,56 @@ ## Promotable Candidate Backlog
|
|||||||
|
|
||||||
**Boundary**: manual promotion only, not auto-prep. These items are intentionally outside `next-best-prep` and require an explicit product decision before any future spec refresh or follow-up work.
|
**Boundary**: manual promotion only, not auto-prep. These items are intentionally outside `next-best-prep` and require an explicit product decision before any future spec refresh or follow-up work.
|
||||||
|
|
||||||
|
### OperationRun Activity Feedback & UI Governance candidate group
|
||||||
|
|
||||||
|
- **Priority posture**: immediate manual-promotion pair, then sequenced strategic follow-ups
|
||||||
|
- **Repo truth**: `OperationRun` is already the execution-truth layer in repo-real code and tests through `OperationUxPresenter`, `OperationStatusNormalizer`, `OperationRunLinks`, `OperationRunUrl`, active-run coverage, notification link contracts, dashboard drill-throughs, and the existing `BulkOperationProgress` Livewire plus poller path. The current progress widget is also a known overlap and UI-drift seam rather than a neutral pattern.
|
||||||
|
- **Why promotable now**: this is the clearest repo-based UX consistency gap around platform execution truth and the best bounded way to stop more surfaces from inventing their own run-state cards, fake progress patterns, or dismiss semantics.
|
||||||
|
- **Why manual promotion only**: the right cut is product-sensitive. The first safe promotion should bundle one bounded v1 execution slice with one durable UI guardrail, while keeping the larger tray, lifecycle, and acknowledgement questions as explicit follow-up candidates instead of hiding them inside the first spec.
|
||||||
|
- **Primary manual-promotion target (promote together)**:
|
||||||
|
- **OperationRun UI Standards & Constitution Guardrail**
|
||||||
|
- capture the shared OperationRun Activity Feedback Pattern in `docs/ui/tenantpilot-enterprise-ui-standards.md`
|
||||||
|
- codify progressbar rules, canonical `View operation` links, dashboard limits, hide/collapse boundaries, and anti-patterns such as fake percentages or floating overlays that cover primary actions
|
||||||
|
- optionally add a constitution-level pointer later if run-surface drift keeps reappearing, but do not block the standards update on a larger constitution rewrite
|
||||||
|
- **OperationRun Activity Feedback v1**
|
||||||
|
- introduce one shared activity/view-model layer over the existing run-status and guidance semantics
|
||||||
|
- introduce one shared compact Filament-native run activity component
|
||||||
|
- adopt that layer in dashboard/start surfaces with at most 1-3 prioritized items
|
||||||
|
- allow determinate progress only from operation-owned counts; otherwise show indeterminate or status-only treatment
|
||||||
|
- support session-local hide/collapse only for non-critical running or queued hints
|
||||||
|
- move `BulkOperationProgress` onto the same model/component family so it stops behaving like a separate visual world
|
||||||
|
- **Guardrails for the primary slice**:
|
||||||
|
- `OperationRun` remains the only run-state truth; dashboards, widgets, notifications, and overlays must not invent a second lifecycle or severity model.
|
||||||
|
- Progressbars are valid only when `total > 0`, `completed <= total`, and the percentage is derived from deterministic run-owned data.
|
||||||
|
- `failed`, `blocked`, `attention_required`, and equivalent follow-up states must not become permanently dismissible.
|
||||||
|
- Dashboard and start surfaces stay decision-first; Operations Hub and run detail stay diagnostics-first.
|
||||||
|
- Every run hint uses the canonical `View operation` link contract.
|
||||||
|
- **Strategic follow-up candidates (after the primary pair)**:
|
||||||
|
1. **Host Widget Operation State Migration Pass**
|
||||||
|
- migrate run-state snippets in host widgets onto the shared inline component without flattening the widget's business role
|
||||||
|
2. **Polling & UI Calmness Standard for Operations**
|
||||||
|
- centralize when polling is allowed, reduce parallel widget polling, and define calm/stale semantics for long-running runs
|
||||||
|
3. **Notification & Operation Activity Lifecycle Standard**
|
||||||
|
- separate toast, DB notification, activity surface, dashboard, Operations Hub, and run-detail responsibilities without creating a second run truth
|
||||||
|
4. **OperationRun Activity Center / Tray v2**
|
||||||
|
- replace or absorb the floating overlay into a calmer, prioritized, non-obstructive activity surface once the shared v1 model exists
|
||||||
|
5. **OperationRun Reviewed / Investigated Semantics v2**
|
||||||
|
- model audited, capability-gated server-side follow-through for problem states without conflating it with session-local hide/collapse
|
||||||
|
- **Anchors**:
|
||||||
|
- `apps/platform/app/Support/OpsUx/OperationUxPresenter.php`
|
||||||
|
- `apps/platform/app/Support/OpsUx/OperationStatusNormalizer.php`
|
||||||
|
- `apps/platform/app/Support/OperationRunLinks.php`
|
||||||
|
- `apps/platform/app/Support/OpsUx/OperationRunUrl.php`
|
||||||
|
- `apps/platform/app/Livewire/BulkOperationProgress.php`
|
||||||
|
- `apps/platform/public/js/tenantpilot/ops-ux-progress-widget-poller.js`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/ActiveRunsTest.php`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/BulkOperationProgressDbOnlyTest.php`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/CanonicalViewRunLinksTest.php`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/NotificationViewRunLinkTest.php`
|
||||||
|
- `apps/platform/tests/Feature/OpsUx/ProgressWidgetOverflowTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Notifications/OperationRunNotificationTest.php`
|
||||||
|
- `apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php`
|
||||||
|
|
||||||
### Decision Register & Approval Workflow v1
|
### Decision Register & Approval Workflow v1
|
||||||
|
|
||||||
- **Priority**: 1
|
- **Priority**: 1
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
# Specification Quality Checklist: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness, boundedness, and readiness before implementation
|
||||||
|
**Created**: 2026-05-03
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] The package stays on one bounded shared lifecycle and retention contract over existing governance artifact families instead of inventing a registry, workflow console, or customer portal.
|
||||||
|
- [x] The spec remains product- and behavior-oriented rather than reading like a low-level implementation diff.
|
||||||
|
- [x] The package explicitly names the repo-real anchors it builds on: `ArtifactTruthPresenter`, review-pack download and suspended-read-only behavior, stored-report fingerprint and prune truth, and append-only accepted-risk decision history.
|
||||||
|
- [x] Mandatory repo sections for scope, RBAC, shared-pattern reuse, testing, proportionality, and candidate rationale are completed.
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No `[NEEDS CLARIFICATION]` markers remain.
|
||||||
|
- [x] Requirements are testable and bounded to current evidence, review, review-pack, customer-workspace, signed-download, stored-report, and accepted-risk aggregate seams only.
|
||||||
|
- [x] The package makes the mutation split gate explicit: hold or deletion-request work ships only when it stays on current owning records, otherwise the slice stops at read-only lifecycle truth plus existing download audit.
|
||||||
|
- [x] Accepted-risk decision adoption remains headless and leaves current Spec 265 browsing and detail surfaces unchanged in this slice.
|
||||||
|
- [x] Canonical proof commands now match across `spec.md`, `plan.md`, `quickstart.md`, and `tasks.md`.
|
||||||
|
- [x] The implementation contract defines reversible `deletion requested` semantics directly and does not rely on an undefined soft-delete requirement.
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] The package forbids new panel, provider, global-search, asset-registration, queue-family, and route-family changes.
|
||||||
|
- [x] Stored reports stay headless and no dedicated stored-report browsing surface is hidden inside this slice.
|
||||||
|
- [x] `CustomerReviewWorkspace` stays read-only and scan-first with no mutation console growth.
|
||||||
|
- [x] Existing Spec 265 decision surfaces are treated as unchanged boundaries, not as delivery scope for this package.
|
||||||
|
- [x] Follow-up ownership stays explicit for Stored Reports Surface, export-before-delete, purge governance, closure lifecycle, and support-access governance.
|
||||||
|
|
||||||
|
## Test Governance
|
||||||
|
|
||||||
|
- [x] Planned proof stays bounded to one new `Unit` suite plus focused `Feature` suites, with browser checking allowed only as a narrow manual exception.
|
||||||
|
- [x] No new heavy-governance, discovery, or browser family is introduced by default.
|
||||||
|
- [x] Fixture growth remains bounded to current factories and `BuildsGovernanceArtifactTruthFixtures` instead of a new full-matrix harness.
|
||||||
|
- [x] The review outcome, workflow outcome, and test-governance outcome are carried into `plan.md`, `quickstart.md`, and `tasks.md`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reviewed against `.specify/memory/constitution.md`, `.specify/templates/checklist-template.md`, `spec.md`, `plan.md`, `research.md`, `data-model.md`, `quickstart.md`, and `tasks.md` on 2026-05-03.
|
||||||
|
- This checklist is the prep-time outcome record. If implementation later widens mutation scope or requires current Spec 265 surface edits, the workflow outcome must change before merge.
|
||||||
|
- Implementation close-out on 2026-05-03 delivered the shared read-only lifecycle and retention contract, validated the existing suspended-read-only and audit seams, and explicitly deferred hold or deletion-request persistence because no bounded current-owner mutation path was implemented in this slice.
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
- **Outcome class**: `acceptable-special-case`
|
||||||
|
- **Workflow outcome**: `split`
|
||||||
|
- **Test-governance outcome**: `keep`
|
||||||
|
- **Reason**: The shipped implementation stays bounded because current operator-facing adoption is limited to existing evidence, review, review-pack, customer-workspace, and signed-download surfaces, while stored reports and accepted-risk decision history stay headless. Hold and deletion-request persistence were not added because the bounded current-owner mutation gate did not pass in this slice, so that work is explicitly split instead of being widened implicitly.
|
||||||
|
- **Final note location**: This checklist during prep, and the active feature PR close-out entry only if implementation forces `split` or `document-in-feature`.
|
||||||
134
specs/267-artifact-lifecycle-retention/data-model.md
Normal file
134
specs/267-artifact-lifecycle-retention/data-model.md
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
# Data Model: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This slice adds one shared derived contract over existing governance artifacts. It does not introduce a generic artifact table, a new browsing console, or a new persistence layer. Any persisted lifecycle or retention markers that v1 truly requires must stay on the existing owning tables or aggregate roots for the concrete artifact families involved.
|
||||||
|
|
||||||
|
## Artifact Family Matrix
|
||||||
|
|
||||||
|
| Artifact family | Owning record | Current immutable or historical anchors | Current lifecycle anchors | Current retention or access anchors | Current operator surfaces | First-slice adoption |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot | `EvidenceSnapshot` | `id`, `fingerprint`, `generated_at` | `status`, `isCurrent()`, latest vs older snapshot truth in the current artifact presenter | `expires_at`; explicit `expire` mutation via `EvidenceSnapshotService` | `EvidenceSnapshotResource`, linked review and pack detail | Visible contract adoption on existing resource and linked surfaces |
|
||||||
|
| Review pack | `ReviewPack` | `id`, `fingerprint`, `previous_fingerprint`, `sha256`, `generated_at` | `status`, relation to `tenant_review_id`, `current_export_review_pack_id` through the review | `expires_at`; signed download allowed only when ready, unexpired, and entitled | `ReviewPackResource`, `ReviewPackDownloadController`, `CustomerReviewWorkspace`, tenant review detail | Visible contract adoption on existing resource, download, and workspace-linked surfaces |
|
||||||
|
| Stored report | `StoredReport` | `id`, `report_type`, `fingerprint`, `previous_fingerprint`, `created_at` | Latest-per-tenant-and-type vs older fingerprint rows is the current or historical distinction | `created_at` plus `tenantpilot.stored_reports.retention_days`; pruning via `PruneStoredReportsCommand` | No dedicated operator resource; evidence sources, widgets, support diagnostics, telemetry | Headless contract adoption only |
|
||||||
|
| Accepted-risk decision record | `FindingExceptionDecision` inside the `FindingException` aggregate | `id`, `decision_type`, `decided_at`, append-only history, parent `current_decision_id` | Parent exception `status`, `current_validity_state`, and current-vs-historical decision relationship | `expires_at`, `review_due_at`, and current validity on the parent exception; no hold or delete contract today | `DecisionRegister`, `ViewFindingException`, finding-governance projections | Headless contract adoption on aggregate models and current findings-service seams only; current register and detail surfaces stay unchanged in this slice |
|
||||||
|
|
||||||
|
`TenantReview` remains a consumer and context surface for linked artifact truth in this slice. Its publication, archive, and successor states must not become retention proxies for the shared contract.
|
||||||
|
|
||||||
|
## Shared Derived Contract
|
||||||
|
|
||||||
|
### GovernanceArtifactReference
|
||||||
|
|
||||||
|
Derived object emitted by the shared lifecycle or truth path.
|
||||||
|
|
||||||
|
| Field | Description | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `family` | One of `evidence_snapshot`, `review_pack`, `stored_report`, `finding_exception_decision` | No synthetic family for a generic registry |
|
||||||
|
| `workspace_id` | Owning workspace scope | Required for all current families |
|
||||||
|
| `tenant_id` | Owning tenant scope | Required for all current in-scope artifact families |
|
||||||
|
| `record_type` | Current model or aggregate owner | Derived from the owning record, not a new registry enum |
|
||||||
|
| `record_id` | Existing primary key | Stable inside the current family |
|
||||||
|
| `display_reference` | Human-readable immutable reference shown on current surfaces | Built from family plus existing identifiers already owned by the record |
|
||||||
|
| `integrity_anchor` | Existing fingerprint, SHA, or append-only decision anchor when available | Nullable; never invent a synthetic hash when the family has no current one |
|
||||||
|
| `source_context` | Existing related artifact or aggregate context | Examples: evidence snapshot, current export pack, parent finding exception |
|
||||||
|
|
||||||
|
Validation rules:
|
||||||
|
|
||||||
|
- `workspace_id` and `tenant_id` remain required scope anchors for every current in-scope family.
|
||||||
|
- The shared contract may normalize display labels, but it must not replace the owning record's real ID or fingerprint.
|
||||||
|
- A direct-access expiry or hold state must not erase the artifact reference from historical views.
|
||||||
|
|
||||||
|
### GovernanceArtifactLifecycleState
|
||||||
|
|
||||||
|
Derived state that explains whether the artifact is the current artifact in active circulation or a retained historical record.
|
||||||
|
|
||||||
|
| Value | Meaning | Repo-grounded examples |
|
||||||
|
|---|---|---|
|
||||||
|
| `current` | Current artifact intended for active operator or customer use | Active evidence snapshot, current export review pack, current decision on the exception aggregate |
|
||||||
|
| `historical` | Retained historical artifact that remains readable without implying a newer replacement path | Older stored reports by fingerprint, prior evidence snapshots still referenced by later reviews |
|
||||||
|
| `superseded` | Historical artifact explicitly replaced by a newer current artifact | Prior export review pack after a newer export becomes current, prior decision row after a newer decision becomes current |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `expired_direct_access` is not a lifecycle state in this slice. It belongs to retention.
|
||||||
|
- `suspended_read_only` is not a lifecycle state in this slice. It remains commercial lifecycle from Spec 251.
|
||||||
|
- A family may map directly from existing repo truth to `historical` without needing a second new state value.
|
||||||
|
|
||||||
|
### GovernanceArtifactRetentionState
|
||||||
|
|
||||||
|
Bounded retention state family used by the shared contract.
|
||||||
|
|
||||||
|
| Value | Meaning | Repo-grounded anchor or note |
|
||||||
|
|---|---|---|
|
||||||
|
| `retained` | Artifact remains retained and readable under normal entitlement rules | Default state for current evidence, packs, stored reports before prune, and historical decisions |
|
||||||
|
| `hold` | Artifact is retained under an explicit hold and cannot progress into deletion | New v1 mutation state only if current-table persistence can stay bounded |
|
||||||
|
| `deletion_requested` | Explicit reversible request to remove the artifact from normal circulation later | New v1 mutation state only if current-table persistence can stay bounded |
|
||||||
|
| `expired_direct_access` | Direct access has expired even though historical reference or audit truth may still remain | Current repo-real pattern for review packs and evidence snapshots with `expires_at` |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `hold` wins over `deletion_requested` for visible truth and progression rules.
|
||||||
|
- `expired_direct_access` does not imply purge, provider deletion, or removal of the immutable reference.
|
||||||
|
- `suspended_read_only` must never be used as a retention-state proxy.
|
||||||
|
- Stored reports may stay `retained` or become effectively unavailable through prune, but prune itself remains a separate retained-history follow-up boundary rather than a generic purge workflow in this spec.
|
||||||
|
|
||||||
|
### GovernanceArtifactActionDecision
|
||||||
|
|
||||||
|
Derived action contract emitted alongside lifecycle and retention truth.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|---|---|
|
||||||
|
| `may_view` | Whether current entitlement and retention state still allow detail access |
|
||||||
|
| `may_download` | Whether current entitlement, retention state, and artifact state still allow direct download of an already-generated artifact |
|
||||||
|
| `may_generate_successor` | Whether a new successor artifact may be generated now |
|
||||||
|
| `may_mutate_lifecycle` | Whether hold, release-hold, or deletion-request actions may execute now |
|
||||||
|
| `blocked_reason` | Customer- or operator-readable reason for the blocked action |
|
||||||
|
| `audit_action` | Stable audit event family for the action or mutation |
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
- `may_generate_successor` remains separate from `may_download`. The repo already proves that a workspace can block new review-pack starts while still allowing ready-pack downloads.
|
||||||
|
- The shared contract should reuse existing policy and capability checks before adding any family-local lifecycle gating.
|
||||||
|
- If a family does not pass the bounded current-owner persistence gate in v1, `may_mutate_lifecycle` remains false or blocked while the contract still exposes truthful retention semantics.
|
||||||
|
|
||||||
|
## Likely Persistence Touch Points
|
||||||
|
|
||||||
|
| Current table or aggregate | Current fields already in repo | Likely v1 additions only if required | Guardrail |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `evidence_snapshots` | `status`, `expires_at`, `fingerprint`, `generated_at` | Family-local hold or deletion-request markers and actor or reason metadata | Do not add a generic artifact foreign key |
|
||||||
|
| `review_packs` | `status`, `expires_at`, `fingerprint`, `previous_fingerprint`, `sha256`, `generated_at` | Family-local hold or deletion-request markers and actor or reason metadata | Keep download truth on the current record and controller path |
|
||||||
|
| `tenant_reviews` | `status`, `published_at`, `archived_at`, `superseded_by_review_id`, `current_export_review_pack_id` | No new retention state; use only to point review surfaces at linked artifact truth | Review lifecycle remains review-owned context, not a shared artifact engine |
|
||||||
|
| `stored_reports` | `report_type`, `fingerprint`, `previous_fingerprint`, `created_at`, `payload` | Prefer derived-only in v1; keep pruning command-owned | No new operator UI surface in this slice |
|
||||||
|
| `finding_exceptions` plus `finding_exception_decisions` | `current_decision_id`, parent `status`, `current_validity_state`, `review_due_at`, `expires_at`, append-only decision rows | If hold or deletion-request semantics are needed, prefer aggregate-level state on `finding_exceptions` rather than mutation of append-only decision history | Do not break append-only history |
|
||||||
|
|
||||||
|
## Query and Precedence Rules
|
||||||
|
|
||||||
|
- `hold` wins over `deletion_requested` for user-visible truth and action blocking.
|
||||||
|
- `expired_direct_access` blocks direct download or open actions only where the current family already uses `expires_at` or equivalent access expiry.
|
||||||
|
- `suspended_read_only` blocks new generation or lifecycle mutation actions, but it does not change lifecycle or retention state by itself.
|
||||||
|
- `ReviewPackDownloadController` remains the canonical enforcement point for ready-pack direct download: entitlement, ready status, unexpired access, and file existence must all remain true.
|
||||||
|
- `StoredReport` current-versus-historical selection should prefer latest report per tenant and report type, with older fingerprint rows remaining historical until pruned.
|
||||||
|
- `FindingExceptionDecision` rows remain historically addressable even when the parent exception points to a newer current decision.
|
||||||
|
|
||||||
|
## State Transitions In Scope
|
||||||
|
|
||||||
|
### Existing repo-real transitions to preserve
|
||||||
|
|
||||||
|
- Evidence snapshot: current active snapshot to expired direct access through `EvidenceSnapshotService::expire(...)`
|
||||||
|
- Review pack: queued to generating to ready or failed, and ready to expired direct access
|
||||||
|
- Finding exception decision history: requested to approved or renewed, rejected, or revoked through append-only decision creation on the parent aggregate
|
||||||
|
- Stored report: retained-by-age to pruned-by-command through `PruneStoredReportsCommand`
|
||||||
|
|
||||||
|
### New bounded transitions this slice may add
|
||||||
|
|
||||||
|
- `retained` to `hold`
|
||||||
|
- `hold` to `retained`
|
||||||
|
- `retained` to `deletion_requested`
|
||||||
|
- `deletion_requested` to `retained`
|
||||||
|
|
||||||
|
Guardrails:
|
||||||
|
|
||||||
|
- No transition in this slice may claim purge or irreversible deletion.
|
||||||
|
- No transition may depend on provider presence or workspace commercial state as its primary meaning.
|
||||||
|
- If a requested transition needs a cross-family executor, scheduler, or export-before-delete workflow, it belongs to a follow-up spec instead of this slice.
|
||||||
|
- If no current family can support these transitions without widening scope, v1 stops at read-only lifecycle truth plus existing download audit and does not add new mutation persistence.
|
||||||
338
specs/267-artifact-lifecycle-retention/plan.md
Normal file
338
specs/267-artifact-lifecycle-retention/plan.md
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
# Implementation Plan: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
**Branch**: `267-artifact-lifecycle-retention` | **Date**: 2026-05-03 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `/specs/267-artifact-lifecycle-retention/spec.md`
|
||||||
|
|
||||||
|
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Prepare one bounded shared lifecycle and retention contract over existing governance artifacts that already exist in repo truth: evidence snapshots, review packs, stored reports, and accepted-risk decision history. The implementation path is to extend the current governance-artifact truth path rather than invent a generic artifact registry, then adopt that contract on the current evidence, review, review-pack, customer-workspace, and signed-download surfaces while keeping stored-report and decision-history adoption headless at the model or aggregate seam in this slice.
|
||||||
|
|
||||||
|
This slice stays explicitly narrow. Filament remains v5 on Livewire v4, panel-provider registration stays in `apps/platform/bootstrap/providers.php`, no new globally searchable resource is introduced, and no asset registration change is expected. The plan does not reopen Spec 262, does not widen billing or closure truth, does not introduce a purge engine or workflow console, and does not force a new Stored Reports or decision-record browsing UI. If hold or deletion-request persistence widens beyond current owning records, the workflow outcome flips to `split` and the shipped slice stops at read-only lifecycle truth plus existing download audit.
|
||||||
|
|
||||||
|
## Inherited Baseline / Explicit Delta
|
||||||
|
|
||||||
|
### Inherited baseline
|
||||||
|
|
||||||
|
- `ArtifactTruthPresenter` already owns shared artifact-truth envelopes and compressed outcomes for evidence and review-pack truth rendered across `EvidenceSnapshot`, `TenantReview`, and `ReviewPack` surfaces.
|
||||||
|
- `EvidenceSnapshotResource`, `TenantReviewResource`, `ReviewPackResource`, and `CustomerReviewWorkspace` already render current operator-facing governance artifact truth or linked artifact context on native Filament surfaces.
|
||||||
|
- `ReviewPackService` already distinguishes future start blocking from retained-history access through `WorkspaceCommercialLifecycleResolver`, and `ReviewPackDownloadController` already preserves ready-pack downloads while a workspace is suspended read-only.
|
||||||
|
- `TenantReviewLifecycleService` already owns publish, archive, and successor-cycle transitions and invalidates derived artifact-truth cache entries.
|
||||||
|
- `EvidenceSnapshotService` already owns direct expiration and audit logging for evidence snapshots.
|
||||||
|
- `StoredReport` already persists report fingerprints and prior fingerprints, and `PruneStoredReportsCommand` already applies age-based retention without a dedicated operator UI.
|
||||||
|
- `FindingException` plus append-only `FindingExceptionDecision` already provide accepted-risk decision history, with existing Spec 265 surfaces remaining unchanged in this slice.
|
||||||
|
|
||||||
|
### Explicit delta in this plan
|
||||||
|
|
||||||
|
- Extend the current governance-artifact truth contract so every in-scope artifact can answer immutable reference, lifecycle role, retention state, and allowed or blocked next action.
|
||||||
|
- Keep the first visible adoption on existing evidence, tenant-review, review-pack, customer-workspace, and signed-download surfaces only.
|
||||||
|
- Add stored-report and decision-history adoption through current services, query seams, and aggregate-level findings seams only.
|
||||||
|
- Keep any new hold, release-hold, or deletion-request mutation scoped to existing detail surfaces and current-table persistence only. If that cannot be done without widening into a registry or purge workflow, split those mutations and ship read-only lifecycle truth first.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4, Laravel 12
|
||||||
|
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, `ArtifactTruthPresenter`, `WorkspaceCommercialLifecycleResolver`, `ReviewPackService`, `TenantReviewLifecycleService`, `FindingExceptionService`, shared badge and RBAC helpers
|
||||||
|
**Storage**: PostgreSQL via existing `evidence_snapshots`, `tenant_reviews`, `review_packs`, `stored_reports`, `finding_exceptions`, `finding_exception_decisions`, workspace and tenant membership tables, and `audit_logs`; no generic artifact table planned
|
||||||
|
**Testing**: Pest v4 `Unit` plus focused `Feature` coverage; browser proof is exception-only if native Filament and controller tests cannot prove the UI or action state cheaply
|
||||||
|
**Validation Lanes**: fast-feedback, confidence
|
||||||
|
**Target Platform**: Laravel monolith in `apps/platform`, using the existing admin plane, tenant-scoped Filament resources, and the existing signed review-pack download route
|
||||||
|
**Project Type**: Web application (Laravel monolith with Filament panels)
|
||||||
|
**Performance Goals**: DB-only render for the affected read surfaces, no new Graph calls, no new queue or `OperationRun` family, and reuse of the existing request-scoped derived-state cache
|
||||||
|
**Constraints**: no purge engine, no workspace or tenant closure flow, no billing or subscription truth work, no support-access governance package, no generic artifact registry UI, no workflow engine, no portal rewrite, no reopening Spec 262, no new asset registration, and no prep-step edits outside `specs/267-artifact-lifecycle-retention/`
|
||||||
|
**Scale/Scope**: 3 existing operator-facing artifact families plus 1 canonical read-only workspace surface and 1 signed download route, with headless contract adoption on 2 additional persisted artifact families and focused extensions to existing aggregate and current-surface test suites
|
||||||
|
|
||||||
|
## Likely Affected Repo Surfaces
|
||||||
|
|
||||||
|
- `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php` as the existing shared contract that already covers evidence, reviews, and review packs but not stored reports or decision records.
|
||||||
|
- `apps/platform/app/Models/EvidenceSnapshot.php`, `apps/platform/app/Models/ReviewPack.php`, `apps/platform/app/Models/StoredReport.php`, `apps/platform/app/Models/FindingException.php`, and `apps/platform/app/Models/FindingExceptionDecision.php` as the artifact-owning records and current lifecycle anchors.
|
||||||
|
- `apps/platform/app/Models/TenantReview.php` plus `apps/platform/app/Services/TenantReviews/TenantReviewLifecycleService.php` as the review-owned context surface that points to the current export artifact without becoming a retention family.
|
||||||
|
- `apps/platform/app/Services/Evidence/EvidenceSnapshotService.php`, `apps/platform/app/Services/ReviewPackService.php`, `apps/platform/app/Services/TenantReviews/TenantReviewLifecycleService.php`, and `apps/platform/app/Services/Findings/FindingExceptionService.php` as the current artifact mutation owners, review-context owners, and audit boundaries.
|
||||||
|
- `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `apps/platform/app/Filament/Resources/TenantReviewResource.php`, `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, and `apps/platform/app/Filament/Resources/ReviewPackResource.php` as the current detail and registry surfaces that already render outcome summaries.
|
||||||
|
- `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` as the current canonical read-only consumption surface that already distinguishes scan-first availability from deeper detail actions.
|
||||||
|
- `apps/platform/app/Models/FindingException.php`, `apps/platform/app/Models/FindingExceptionDecision.php`, and `apps/platform/app/Services/Findings/FindingExceptionService.php` as the accepted-risk aggregate seams that may adopt the shared contract without reopening current Spec 265 surfaces.
|
||||||
|
- `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` as the existing ready-pack, signed-download, and download-audit seam.
|
||||||
|
- `apps/platform/app/Console/Commands/PruneStoredReportsCommand.php` plus the stored-report-producing services under `Services/PermissionPosture/` and `Services/EntraAdminRoles/` as the current retention path for stored reports.
|
||||||
|
- Existing related tests under `apps/platform/tests/Feature/Evidence/`, `apps/platform/tests/Feature/ReviewPack/`, `apps/platform/tests/Feature/Reviews/`, `apps/platform/tests/Feature/TenantReview/`, `apps/platform/tests/Feature/Governance/`, `apps/platform/tests/Feature/Findings/`, `apps/platform/tests/Feature/PermissionPosture/`, and `apps/platform/tests/Feature/EntraAdminRoles/`, plus `apps/platform/tests/Feature/Concerns/BuildsGovernanceArtifactTruthFixtures.php`.
|
||||||
|
|
||||||
|
## UI / Filament & Livewire Fit
|
||||||
|
|
||||||
|
- Evidence snapshots, tenant-review detail summaries, review packs, and the customer review workspace already sit on native Filament v5 surfaces under Livewire v4. This slice should stay inside those surfaces instead of adding a new panel, portal shell, or artifact-management console.
|
||||||
|
- `EvidenceSnapshotResource`, `TenantReviewResource`, and `ReviewPackResource` already declare read-only registry-report action surfaces and already set `protected static bool $isGloballySearchable = false;`. That posture stays unchanged for this slice.
|
||||||
|
- The visible lifecycle contract should continue to flow through the existing `Outcome summary`, compressed outcome badges, and current context-link sections. Do not create a second page-local lifecycle badge map or duplicate summary block.
|
||||||
|
- Existing destructive-like actions already show the expected Filament pattern: evidence snapshot `expire`, review-pack `expire`, and tenant-review `archive_review` already live on existing surfaces with `->action(...)`, `->requiresConfirmation()`, and server-side authorization. Any new `Place hold`, `Release hold`, or `Request deletion` action must follow that same pattern and must stay off `CustomerReviewWorkspace` and `DecisionRegister`.
|
||||||
|
- Stored reports and accepted-risk decisions do not justify a new operator browsing surface in this slice. Stored reports should stay headless, and decision-history adoption should stay inside the current aggregate and findings-service seams without changing current Spec 265 surfaces.
|
||||||
|
- No new panel-provider, global-search, or asset-registration work is planned. If implementation later proves a shared asset is necessary anyway, deployment remains the normal `cd apps/platform && php artisan filament:assets` path.
|
||||||
|
|
||||||
|
## RBAC / Policy Fit
|
||||||
|
|
||||||
|
- Workspace and tenant membership remain isolation boundaries. Non-members or wrong-scope actors must keep receiving `404`, while in-scope members missing the relevant capability keep receiving `403`.
|
||||||
|
- Existing controller and resource behavior already demonstrates the intended boundary: `ReviewPackDownloadController` checks tenant access first, capability second, and only then artifact state.
|
||||||
|
- The slice should reuse the current capability registry and policy/service owners rather than inventing a new lifecycle capability family. Existing `REVIEW_PACK_VIEW`, `REVIEW_PACK_MANAGE`, `TENANT_REVIEW_VIEW`, `TENANT_REVIEW_MANAGE`, `EVIDENCE_VIEW`, and `FINDING_EXCEPTION_*` checks remain authoritative.
|
||||||
|
- Suspended read-only posture remains a business-state overlay, not a retention-state proxy. `ReviewPackService` already blocks new pack starts through `WorkspaceCommercialLifecycleResolver` while `ReviewPackDownloadTest` proves that ready-pack downloads remain allowed.
|
||||||
|
- Decision-record adoption must preserve the current Spec 265 audience boundary by leaving `DecisionRegister` and `ViewFindingException` unchanged in this slice. Accepted-risk lifecycle mapping stays on the aggregate and current findings-service seams.
|
||||||
|
|
||||||
|
## Audit / Logging Fit
|
||||||
|
|
||||||
|
- Existing artifact-sensitive actions already have audit anchors that this slice should reuse rather than replace: `ReviewPackDownloaded`, `EvidenceSnapshotExpired`, tenant-review publish or archive events, and finding-exception request, approval, renewal, rejection, and revocation events.
|
||||||
|
- `ReviewPackDownloadController` already records artifact, actor, workspace, tenant, source surface, and operation-run context. The lifecycle contract should extend that audit language rather than creating a second download ledger.
|
||||||
|
- `EvidenceSnapshotService` and `ReviewPackService` already own audit-backed lifecycle mutation paths, while `TenantReviewLifecycleService` remains review-owned context. If hold or deletion-request actions are added in v1, they should stay on the current artifact owners with stable action IDs and without widening into cross-family orchestration.
|
||||||
|
- Stored-report creation already has telemetry and pruning already has command coverage, but there is no dedicated operator mutation surface today. This slice should not create a new stored-report audit subsystem just to compensate for the absence of a browsing UI.
|
||||||
|
- Passive page renders stay non-audited. Audit remains tied to explicit artifact opens, downloads, or lifecycle mutations.
|
||||||
|
|
||||||
|
## Data & Query Fit
|
||||||
|
|
||||||
|
- The shared contract must stay derived-first. Extend the current artifact-truth envelope and compression path before considering any new persistence, and do not create a generic artifact super-table or shadow projection.
|
||||||
|
- Current repo-real retention anchors already exist and should be reused: `EvidenceSnapshot.expires_at`, `ReviewPack.expires_at`, `ReviewPack.status`, `StoredReport.created_at` plus `stored_reports.retention_days`, and append-only `FindingExceptionDecision.decided_at` plus `FindingException.current_decision_id` and status or validity on the parent exception.
|
||||||
|
- Current immutable or historical anchors already exist per family: evidence snapshot fingerprints, review fingerprints, review-pack fingerprint and SHA-256, stored-report fingerprint plus `previous_fingerprint`, and decision-record ID plus `decided_at` history on append-only rows.
|
||||||
|
- `ReviewPackService` already distinguishes reusable current packs from expired direct access through fingerprint lookup and `expires_at > now()`. The new contract should preserve that query truth instead of duplicating it.
|
||||||
|
- `CustomerReviewWorkspace` and current review-pack or review-detail surfaces already derive “what remains readable now” from existing relations and capabilities. The new contract should feed those surfaces, not replace them with a new cross-artifact query model.
|
||||||
|
- Stored-report adoption should stay in current service and command seams. `PruneStoredReportsCommand` is the current retention executor, so stored-report lifecycle adoption in this slice should clarify truth and historical identity but must not turn pruning into a generic purge engine.
|
||||||
|
- Accepted-risk decision adoption should attach to `FindingException` plus `FindingExceptionDecision` and the current findings-service or aggregate seams. Do not remodel decision history into a second artifact aggregate or a decision-surface rewrite.
|
||||||
|
- If hold or deletion-request markers must become persistent in v1, they should be added only to the current owning tables or aggregate roots for the concrete families involved. If that work starts to require a cross-family artifact table, a decision-surface rewrite, or shared orchestration, split the mutation slice.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: changed surfaces
|
||||||
|
- **Native vs custom classification summary**: native Filament
|
||||||
|
- **Shared-family relevance**: evidence and report viewers, status messaging, lifecycle badges, download actions, canonical read-only availability, aggregate-level decision-history continuity
|
||||||
|
- **State layers in scope**: page, detail, URL-query
|
||||||
|
- **Audience modes in scope**: customer/read-only, operator-MSP, support-platform
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
|
||||||
|
- **Raw/support gating plan**: capability-gated and hidden by default through existing detail surfaces or support diagnostics only
|
||||||
|
- **One-primary-action / duplicate-truth control**: evidence, review, and review-pack surfaces keep one dominant current action and one summary statement of lifecycle truth; `CustomerReviewWorkspace` keeps `Open review` as the dominant row affordance and does not become a download console
|
||||||
|
- **Handling modes by drift class or surface**: review-mandatory
|
||||||
|
- **Repository-signal treatment**: review-mandatory now, future hard-stop candidate if implementation adds a new artifact console or local lifecycle taxonomy
|
||||||
|
- **Special surface test profiles**: standard-native-filament, shared-detail-family
|
||||||
|
- **Required tests or manual smoke**: functional-core, state-contract
|
||||||
|
- **Exception path and spread control**: none planned; any new registry UI, page-local badge map, or browser-heavy proof demand is exception-required drift
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes
|
||||||
|
- **Systems touched**: `ArtifactTruthPresenter`, `BadgeCatalog` and `BadgeRenderer`, Filament action-surface declarations, `WorkspaceCommercialLifecycleResolver`, `ReviewPackService`, `ReviewPackDownloadController`, `CustomerReviewWorkspace`, `FindingException`, `FindingExceptionDecision`, `FindingExceptionService`, `CanonicalNavigationContext`, and existing audit loggers
|
||||||
|
- **Shared abstractions reused**: current `ArtifactTruthPresenter` and derived-state cache, current badge domains, current Filament resource and page action-surface contracts, current commercial lifecycle resolver, current audit paths, and existing decision register continuity helpers
|
||||||
|
- **New abstraction introduced? why?**: none by default. The narrowest acceptable addition is a bounded lifecycle or retention mapper inside the existing governance-artifact-truth support namespace if the presenter becomes unreadable without decomposition
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: the current presenter is sufficient for evidence, reviews, and review packs, but insufficient because it does not yet carry immutable reference plus retention semantics and does not support stored reports or decision records
|
||||||
|
- **Bounded deviation / spread control**: stored-report and decision-record adoption must extend the shared artifact-truth path or a bounded helper beneath it. They must not create a parallel report-truth or decision-truth presenter family
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: yes, by reuse only
|
||||||
|
- **Central contract reused**: existing review-pack start and completion UX through `OperationRunService` and `OperationUxPresenter`
|
||||||
|
- **Delegated UX behaviors**: existing queued toast, dedupe behavior, and run-link semantics stay unchanged when review-pack generation is allowed; blocked starts remain pre-run business-state blocks with no new run created
|
||||||
|
- **Surface-owned behavior kept local**: detail surfaces may explain lifecycle or retention truth, but they must not create a second operation-start UX language for future export or deletion flows
|
||||||
|
- **Queued DB-notification policy**: unchanged
|
||||||
|
- **Terminal notification path**: unchanged central lifecycle mechanism for existing review-pack runs only
|
||||||
|
- **Exception path**: none planned. Any future long-running export-before-delete or purge work belongs to a named follow-up slice
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Provider-owned seams**: provider content inside evidence snapshots and stored-report payloads stays provider-owned evidence
|
||||||
|
- **Platform-core seams**: governance artifact reference, lifecycle state, retention state, historical readability, hold semantics, deletion-request semantics, and suspended read-only access rules
|
||||||
|
- **Neutral platform terms / contracts preserved**: `governance artifact`, `artifact reference`, `lifecycle state`, `retention state`, `historical`, `superseded`, `retained`, `hold`, `deletion requested`, and `download allowed`
|
||||||
|
- **Retained provider-specific semantics and why**: evidence completeness and provider-derived content remain inside the existing evidence or report truth summaries because this slice governs TenantPilot-owned artifacts, not provider object lifecycle
|
||||||
|
- **Bounded extraction or follow-up path**: follow-up-spec for provider-lifecycle expansion beyond the current evidence and report truth already in repo
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before implementation begins and again before merge.*
|
||||||
|
|
||||||
|
- Inventory-first / snapshot truth: PASS. The slice governs TenantPilot-owned persisted artifacts and their existing historical truth, not provider live state.
|
||||||
|
- Read/write separation: PASS. The dominant rollout work is read-surface truth adoption. Any new lifecycle mutation remains local-only, confirmation-backed, audit-backed, and on existing detail surfaces.
|
||||||
|
- Graph contract path: PASS. No new Graph call path is introduced.
|
||||||
|
- Deterministic capabilities: PASS. Existing capability registries, policies, and controller checks stay authoritative.
|
||||||
|
- Workspace and tenant isolation: PASS. All current surfaces remain workspace- and tenant-safe, and canonical read-only views keep current entitlement boundaries.
|
||||||
|
- RBAC-UX plane separation: PASS. Everything stays inside current `/admin` and tenant-scoped surfaces; no `/system` or second artifact portal is introduced.
|
||||||
|
- Destructive action discipline: PASS. Existing evidence, review-pack, and tenant-review destructive-like actions already use confirmation, and any new hold or deletion-request action must follow the same `->action(...)` plus `->requiresConfirmation()` rule.
|
||||||
|
- Global search safety: PASS. `EvidenceSnapshotResource`, `TenantReviewResource`, and `ReviewPackResource` already disable global search, no new searchable resource is added, and current pages do not become searchable.
|
||||||
|
- OperationRun / Ops-UX: PASS by reuse only. Existing review-pack generation keeps the shared start UX; blocked starts create no run; no new run family is introduced.
|
||||||
|
- Data minimization: PASS. Default-visible lifecycle truth stays concise, while raw report payloads, fingerprints, support diagnostics, and detailed reasons remain secondary.
|
||||||
|
- Test governance: PASS. Proof stays in focused unit plus feature suites, with browser proof explicitly excluded from the default plan.
|
||||||
|
- Proportionality / no premature abstraction: PASS. The narrowest correct path is to extend the current presenter and existing tables or aggregates rather than adding a registry or workflow engine.
|
||||||
|
- Persisted truth: PASS. No new generic table or shadow artifact entity is planned.
|
||||||
|
- Behavioral state: PASS. Lifecycle and retention states change allowed actions, read-only interpretation, audit responsibilities, and retention semantics.
|
||||||
|
- Shared pattern first / UI semantics / Filament-native UI: PASS. Existing badges, artifact-truth presentation, detail pages, and canonical read-only workspace surface remain the shared path.
|
||||||
|
- Provider boundary: PASS. Provider evidence stays provider-owned and does not become lifecycle truth.
|
||||||
|
- Filament / Laravel planning contract: PASS. Filament v5 stays on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, no new globally searchable resource is added, and no asset registration change is planned.
|
||||||
|
|
||||||
|
**Gate evaluation**: PASS.
|
||||||
|
|
||||||
|
**Post-design re-check**: PASS (design artifacts: [research.md](research.md), [data-model.md](data-model.md), [quickstart.md](quickstart.md); no `contracts/` directory by design because the slice reuses existing Filament resources and the existing signed download route rather than introducing a new HTTP contract).
|
||||||
|
|
||||||
|
**Agent-context refresh note**: intentionally not applied in this preparation run because the task is explicitly limited to `specs/267-artifact-lifecycle-retention/` and the plan introduces no new language, framework, storage, or deployment technology.
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: `Unit` for shared lifecycle and retention mapping in the artifact-truth support layer; `Feature` for resource, detail, controller, command, and read-only workspace behavior
|
||||||
|
- **Affected validation lanes**: fast-feedback, confidence
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: the contract is already consumed through native Filament resources, controllers, and existing commands, so focused unit and feature coverage can prove lifecycle mapping, authorization, audit, and read-only behavior without a browser lane by default
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/GovernanceArtifactTruth/GovernanceArtifactLifecycleContractTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewLifecycleTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingExceptionRevocationTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/StoredReportModelTest.php tests/Feature/PermissionPosture/PruneStoredReportsCommandTest.php tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: moderate but contained; reuse `BuildsGovernanceArtifactTruthFixtures`, existing review-pack and customer-workspace fixtures, and current finding-exception setup helpers instead of inventing a new governance matrix harness
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no; any new helper should stay in the existing governance-artifact-truth support layer or current family fixtures and remain opt-in
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none planned; browser proof stays out of the default plan
|
||||||
|
- **Surface-class relief / special coverage rule**: standard-native-filament relief for evidence, review, and review-pack surfaces; shared-detail-family coverage for customer-workspace launch and aggregate decision-history continuity; controller and command tests cover signed download and stored-report pruning
|
||||||
|
- **Closing validation and reviewer handoff**: reviewers should rely on the exact commands above, confirm that no new searchable resource or asset change slipped in, verify that `CustomerReviewWorkspace` stays read-only, verify that stored-report and decision-history adoption stayed headless, and verify that any mutation work that widened beyond current owners resolved as `split`
|
||||||
|
- **Budget / baseline / trend follow-up**: none expected beyond a modest feature-local increase in unit and feature coverage
|
||||||
|
- **Review-stop questions**: did the implementation add a registry or workflow layer, did browser-only assertions sneak into the default proof, did stored-report or decision-record work create a new UI, and did hold or deletion-request persistence widen beyond current tables
|
||||||
|
- **Escalation path**: `document-in-feature` for contained mapping or audit wording drift; `reject-or-split` if implementation introduces a new artifact console, generic table, or browser-heavy default proof
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
- **Why no dedicated follow-up spec is needed**: routine lifecycle-truth and retention-truth upkeep should stay inside this feature unless implementation proves a structural need for purge, closure, export-before-delete, or a new artifact family surface
|
||||||
|
- **Test-governance outcome**: keep
|
||||||
|
|
||||||
|
## Review Checklist Status
|
||||||
|
|
||||||
|
- **Review checklist artifact**: [checklists/requirements.md](checklists/requirements.md)
|
||||||
|
- **Review outcome class**: `acceptable-special-case`
|
||||||
|
- **Workflow outcome**: `keep`
|
||||||
|
- **Test-governance outcome**: `keep`
|
||||||
|
- **Escalation rule**: if mutation persistence widens beyond current owners or a current decision-surface edit becomes necessary, flip the workflow outcome to `split` before implementation continues
|
||||||
|
|
||||||
|
## Rollout Considerations
|
||||||
|
|
||||||
|
- Roll out the shared contract first, then the current evidence, review, review-pack, customer-workspace, and signed-download surfaces, and only then the headless stored-report and aggregate-level decision-history adoption.
|
||||||
|
- Keep `CustomerReviewWorkspace` scan-first, and keep current Spec 265 surfaces unchanged in this slice. Neither should become the mutation surface for lifecycle actions.
|
||||||
|
- Preserve the current suspended-read-only split that the repo already proves: generation or mutation can block while retained history and already-generated downloads remain readable when the retention state allows it.
|
||||||
|
- Keep Filament v5 on Livewire v4, keep panel providers in `apps/platform/bootstrap/providers.php`, keep `EvidenceSnapshotResource`, `TenantReviewResource`, and `ReviewPackResource` non-searchable, and keep the current asset strategy unchanged.
|
||||||
|
- Do not add a contracts package or new route in v1. Existing route names, resource URLs, and controller entry points remain the canonical surface inventory.
|
||||||
|
|
||||||
|
## Risk Controls
|
||||||
|
|
||||||
|
- Reject any implementation that adds a generic artifact registry table, artifact console, workflow engine, or customer artifact portal.
|
||||||
|
- Reject any implementation that creates a second lifecycle presenter family outside the current governance-artifact-truth support layer.
|
||||||
|
- Reject any implementation that uses workspace commercial state, provider absence, review publication status, or page-local copy as a proxy for retention truth.
|
||||||
|
- Reject any implementation that places hold or deletion-request actions on `CustomerReviewWorkspace`, current Spec 265 surfaces, or other list-only shells.
|
||||||
|
- Reject browser-heavy proof as the default lane. Escalate to a browser smoke only if grouped actions or read-only detail behavior cannot be proven through native feature tests.
|
||||||
|
- If hold or deletion-request persistence cannot stay on current family tables or aggregate roots without widening scope, split those mutations and ship the shared read-only contract first.
|
||||||
|
|
||||||
|
## Research & Design Outputs
|
||||||
|
|
||||||
|
- [research.md](research.md) resolves the key design choices: extend the current artifact-truth presenter, keep stored reports headless, keep decision-history adoption on current aggregate seams, and preserve suspended-read-only download semantics.
|
||||||
|
- [data-model.md](data-model.md) records the bounded derived contract, current lifecycle and retention anchors per family, likely current-table persistence touch points, and precedence rules such as hold over deletion request.
|
||||||
|
- [quickstart.md](quickstart.md) provides the recommended implementation order, focused validation commands, and stop conditions for keeping the slice bounded.
|
||||||
|
- [checklists/requirements.md](checklists/requirements.md) records the preparation review outcome, workflow outcome, and test-governance outcome that implementation must preserve unless the slice is explicitly split.
|
||||||
|
- No `contracts/` directory is created because this slice introduces no new HTTP route, controller family, or external integration contract.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/267-artifact-lifecycle-retention/
|
||||||
|
├── checklists/
|
||||||
|
│ └── requirements.md
|
||||||
|
├── plan.md
|
||||||
|
├── research.md
|
||||||
|
├── data-model.md
|
||||||
|
├── quickstart.md
|
||||||
|
└── tasks.md # Created later by /speckit.tasks, not by this plan step
|
||||||
|
```
|
||||||
|
|
||||||
|
This preparation package intentionally stays small. The plan, research, data-model, and quickstart artifacts are enough to drive tasks generation for a bounded implementation. A separate `contracts/` package would duplicate existing route and resource truth.
|
||||||
|
|
||||||
|
### Source Code (expected implementation surfaces)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/
|
||||||
|
│ │ ├── Pages/
|
||||||
|
│ │ │ └── Reviews/
|
||||||
|
│ │ │ └── CustomerReviewWorkspace.php
|
||||||
|
│ │ └── Resources/
|
||||||
|
│ │ ├── EvidenceSnapshotResource.php
|
||||||
|
│ │ ├── ReviewPackResource.php
|
||||||
|
│ │ ├── TenantReviewResource.php
|
||||||
|
│ │ └── TenantReviewResource/Pages/ViewTenantReview.php
|
||||||
|
│ ├── Http/Controllers/ReviewPackDownloadController.php
|
||||||
|
│ ├── Models/
|
||||||
|
│ │ ├── EvidenceSnapshot.php
|
||||||
|
│ │ ├── ReviewPack.php
|
||||||
|
│ │ ├── StoredReport.php
|
||||||
|
│ │ ├── TenantReview.php
|
||||||
|
│ │ ├── FindingException.php
|
||||||
|
│ │ └── FindingExceptionDecision.php
|
||||||
|
│ ├── Services/
|
||||||
|
│ │ ├── Evidence/EvidenceSnapshotService.php
|
||||||
|
│ │ ├── Findings/FindingExceptionService.php
|
||||||
|
│ │ ├── ReviewPackService.php
|
||||||
|
│ │ └── TenantReviews/TenantReviewLifecycleService.php
|
||||||
|
│ ├── Support/
|
||||||
|
│ │ └── Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php
|
||||||
|
│ └── Console/Commands/PruneStoredReportsCommand.php
|
||||||
|
├── bootstrap/providers.php
|
||||||
|
└── tests/
|
||||||
|
├── Feature/Evidence/
|
||||||
|
├── Feature/ReviewPack/
|
||||||
|
├── Feature/Reviews/
|
||||||
|
├── Feature/TenantReview/
|
||||||
|
├── Feature/Findings/
|
||||||
|
├── Feature/PermissionPosture/
|
||||||
|
├── Feature/EntraAdminRoles/
|
||||||
|
└── Unit/Support/GovernanceArtifactTruth/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: Laravel monolith. Implementation should stay inside the existing artifact-truth support layer plus the named current model, service, resource, page, controller, and test seams. No new panel, artifact sub-app, or generic registry package is justified.
|
||||||
|
|
||||||
|
## Complexity Tracking
|
||||||
|
|
||||||
|
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||||
|
|-----------|------------|-------------------------------------|
|
||||||
|
| BLOAT-001 - bounded lifecycle and retention contract extension | The repo already has three operator-facing artifact families plus stored-report and decision-record truth that need one shared meaning for reference, lifecycle role, retention posture, and next action | Local page labels would drift immediately, while a generic artifact registry would import persistence and UI scope the current release does not need |
|
||||||
|
|
||||||
|
## Proportionality Review
|
||||||
|
|
||||||
|
- **Current operator problem**: current artifact surfaces still make operators infer whether an artifact is current, historical, expired from direct access, still downloadable, or merely blocked by read-only workspace posture.
|
||||||
|
- **Existing structure is insufficient because**: the current presenter answers outcome and readiness truth for evidence, reviews, and review packs, but not immutable reference plus retention semantics, and it does not support stored reports or accepted-risk decision records.
|
||||||
|
- **Narrowest correct implementation**: extend the existing artifact-truth path, keep persistence local to current tables or aggregates only where needed, adopt the contract on current surfaces first, and leave stored reports headless plus decision history on current aggregates without a decision-surface rewrite.
|
||||||
|
- **Ownership cost created**: one bounded lifecycle vocabulary, one bounded retention vocabulary, one presenter extension, current-table persistence only where mutation is required, and focused test expansion across existing suites.
|
||||||
|
- **Alternative intentionally rejected**: a generic artifact registry table, artifact-management console, workflow engine, customer artifact portal, or broad purge framework was rejected as wider than current release truth.
|
||||||
|
- **Release truth**: current-release truth grounded in already-persisted artifacts, already-shipped suspended-read-only behavior, and already-existing accepted-risk decision history.
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1 - Extend the shared contract
|
||||||
|
|
||||||
|
- Extend the governance-artifact truth support layer so `EvidenceSnapshot` and `ReviewPack` can emit immutable reference, lifecycle state, retention state, and allowed or blocked next-action semantics from one path, while `TenantReview` consumes linked artifact truth without becoming its own retention family.
|
||||||
|
- Keep lifecycle and retention mapping bounded and family-aware. Do not create a new generic registry or polymorphic persistence layer.
|
||||||
|
- Decide the smallest acceptable current-table persistence for hold or deletion-request markers, and stop if that work starts to require a cross-family artifact store.
|
||||||
|
|
||||||
|
### Phase 2 - Adopt the current operator surfaces
|
||||||
|
|
||||||
|
- Apply the contract to `EvidenceSnapshotResource`, `TenantReviewResource`, `ViewTenantReview`, `ReviewPackResource`, `CustomerReviewWorkspace`, and `ReviewPackDownloadController`.
|
||||||
|
- Preserve the existing scan-first and read-first surface contracts: one dominant next action, no new console, and no duplicate lifecycle summary blocks.
|
||||||
|
- Keep the current commercial-lifecycle split: future generation or mutation may block, while retained history and already-generated downloads remain available when the retention state allows it.
|
||||||
|
|
||||||
|
### Phase 3 - Add headless stored-report and decision-record adoption
|
||||||
|
|
||||||
|
- Extend the shared contract to `StoredReport` and accepted-risk decision history through current services, aggregates, and tests.
|
||||||
|
- Keep stored reports without a new browsing UI. Use the existing creation, fingerprint, evidence-source, and prune seams instead.
|
||||||
|
- Keep decision-history adoption inside `FindingException`, `FindingExceptionDecision`, and `FindingExceptionService` without reopening Spec 265 into a new console or workflow.
|
||||||
|
|
||||||
|
### Phase 4 - Harden audit, RBAC, and proof
|
||||||
|
|
||||||
|
- Extend focused unit plus feature suites for lifecycle mapping, current vs historical truth, retention-state gating, signed download preservation, audit coverage, and 404 or 403 semantics.
|
||||||
|
- Confirm no new searchable resources, no provider-registration changes, no asset-registration changes, and no browser lane by default.
|
||||||
|
- Stop after the bounded contract and current-surface adoption are proven. If mutation persistence widens beyond current owners, split that work and defer it along with purge, closure, export-before-delete, or broader support-access work.
|
||||||
|
|
||||||
|
## Deferred Follow-ups
|
||||||
|
|
||||||
|
- **Retention & Purge Governance v1** owns irreversible deletion, purge scheduling, purge proof, and any true hard-delete execution.
|
||||||
|
- **Workspace & Tenant Closure Lifecycle v1** owns closure-class behavior for broader workspace and tenant truth.
|
||||||
|
- **Data Export Before Deletion v1** owns export-request workflow, completion proof, and delete preconditions for customer-owned data bundles.
|
||||||
|
- **Stored Reports Surface v1** owns any future dedicated stored-report browsing and operator workflow.
|
||||||
|
- **Enterprise Access Boundary & Support Access Governance v1** owns support-access approval, TTL, and customer-visible support access audit rather than artifact retention truth.
|
||||||
|
|
||||||
|
## Why This Plan Is Narrow Enough
|
||||||
|
|
||||||
|
The repo already has the hard parts needed for a truthful first slice: persisted artifact families, current evidence or review or pack surfaces, a shared artifact-truth presenter for the visible families, commercial read-only gating for review-pack starts versus downloads, append-only accepted-risk decision history, and current audit paths. The plan therefore adds only the missing shared lifecycle and retention contract over those real seams, keeps stored-report and decision-history adoption tied to their current owners, leaves current Spec 265 surfaces unchanged, and leaves everything broader to explicit follow-up work.
|
||||||
57
specs/267-artifact-lifecycle-retention/quickstart.md
Normal file
57
specs/267-artifact-lifecycle-retention/quickstart.md
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
# Quickstart: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Implement one bounded shared lifecycle and retention contract over the existing governance-artifact families without widening into a registry UI, purge engine, closure flow, billing overlay, or new browsing console.
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Extend the current governance-artifact truth support layer.
|
||||||
|
2. Apply the shared contract to evidence, tenant-review, review-pack, customer-workspace, and signed-download surfaces.
|
||||||
|
3. Add stored-report and accepted-risk decision-history adoption through existing headless model, aggregate, and service seams.
|
||||||
|
4. Add family-local hold or deletion-request persistence only if it can stay on current tables or aggregates without widening scope; otherwise stop at read-only lifecycle truth plus existing download audit.
|
||||||
|
5. Run the focused unit and feature proof and stop.
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
1. Add bounded lifecycle and retention mapping to the current artifact-truth path.
|
||||||
|
2. Keep `EvidenceSnapshotResource`, `TenantReviewResource`, `ViewTenantReview`, `ReviewPackResource`, and `CustomerReviewWorkspace` on their current action-surface contracts.
|
||||||
|
3. Preserve the existing review-pack split: blocked future starts may show a business-state block, but ready retained downloads stay on the current controller path.
|
||||||
|
4. Keep `StoredReport` adoption headless through current service, fingerprint, and prune seams.
|
||||||
|
5. Keep decision-history adoption inside `FindingException`, `FindingExceptionDecision`, and `FindingExceptionService` without introducing a second decision console or rewriting current Spec 265 surfaces.
|
||||||
|
6. Only add new destructive-like actions on current detail surfaces with `->action(...)`, `->requiresConfirmation()`, and server-side authorization after the bounded current-owner persistence gate passes.
|
||||||
|
7. Stop if implementation starts demanding a generic artifact table, new console, or browser-heavy proof by default.
|
||||||
|
|
||||||
|
## Focused Proof Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/GovernanceArtifactTruth/GovernanceArtifactLifecycleContractTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewLifecycleTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingExceptionRevocationTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/StoredReportModelTest.php tests/Feature/PermissionPosture/PruneStoredReportsCommandTest.php tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Checks Only If Native Proof Is Insufficient
|
||||||
|
|
||||||
|
1. Open a review-pack detail that is ready, retained, and downloadable, then confirm the surface states reference, lifecycle truth, retention truth, and allowed next action without opening diagnostics.
|
||||||
|
2. Put the workspace into suspended read-only posture and confirm that review-pack generation still blocks before creating a run while signed ready-pack downloads remain available.
|
||||||
|
3. Open the customer review workspace and confirm it stays scan-first with `Open review` as the dominant row affordance rather than a new download or mutation console.
|
||||||
|
4. Open an evidence snapshot detail and confirm it shows linked artifact reference, lifecycle truth, retention truth, and blocked-reason wording without turning the page into a mutation console.
|
||||||
|
|
||||||
|
## Stop Conditions
|
||||||
|
|
||||||
|
- A generic artifact registry table, artifact console, or workflow engine becomes necessary.
|
||||||
|
- Hold or deletion-request persistence cannot stay on current family tables or aggregate roots.
|
||||||
|
- Accepted-risk decision adoption requires a current-slice `DecisionRegister` or `ViewFindingException` rewrite instead of staying headless.
|
||||||
|
- The slice starts to require purge, closure, export-before-delete, or support-access workflow semantics.
|
||||||
|
- Browser tests become the default proving lane instead of a bounded exception.
|
||||||
|
- The implementation proposes new global-search resources, panel-provider changes, or asset-registration changes for this slice.
|
||||||
|
|
||||||
|
## Review Close-out
|
||||||
|
|
||||||
|
1. Re-check `specs/267-artifact-lifecycle-retention/checklists/requirements.md` before implementation and close-out.
|
||||||
|
2. Keep the review outcome class at `acceptable-special-case`, the workflow outcome at `keep`, and the test-governance outcome at `keep` unless the mutation split gate or a decision-surface rewrite forces escalation.
|
||||||
|
3. If the bounded current-owner persistence gate fails, flip the workflow outcome to `split` before continuing.
|
||||||
58
specs/267-artifact-lifecycle-retention/research.md
Normal file
58
specs/267-artifact-lifecycle-retention/research.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Research: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
**Branch**: `267-artifact-lifecycle-retention`
|
||||||
|
**Date**: 2026-05-03
|
||||||
|
|
||||||
|
## Decision 1 - Extend the current governance-artifact truth path
|
||||||
|
|
||||||
|
- **Decision**: Extend `ArtifactTruthPresenter` and its existing derived-state path instead of creating a second lifecycle presenter or a generic artifact registry.
|
||||||
|
- **Rationale**: The repo already uses `ArtifactTruthPresenter` for `EvidenceSnapshot`, `TenantReview`, and `ReviewPack`, and the visible Filament surfaces already depend on that path for outcome summary and compressed outcome rendering. The missing gap is support for immutable reference plus retention semantics, and support for `StoredReport` and decision-record families.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Page-local lifecycle labels on each resource: rejected because evidence, review, review-pack, customer-workspace, and decision surfaces would drift immediately.
|
||||||
|
- Generic artifact super-table or registry UI: rejected because it would import persistence, migration, and browsing scope beyond current release truth.
|
||||||
|
|
||||||
|
## Decision 2 - Keep the first visible adoption on current surfaces only
|
||||||
|
|
||||||
|
- **Decision**: Limit operator-facing adoption to `EvidenceSnapshotResource`, `TenantReviewResource`, `ViewTenantReview`, `ReviewPackResource`, `CustomerReviewWorkspace`, and `ReviewPackDownloadController`, while keeping stored-report and accepted-risk decision-history adoption headless.
|
||||||
|
- **Rationale**: Those surfaces already answer artifact availability and next-step questions today. Stored reports still have no dedicated operator surface, and current Spec 265 surfaces already own accepted-risk browsing and detail workflows, so this slice should not rewrite `DecisionRegister` or `ViewFindingException`.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- New artifact-management console: rejected because the user explicitly wants the slice to stay on current evidence, review, review-pack, and download surfaces.
|
||||||
|
- New stored-report or decision-record resource: rejected because that would hide product-surface work inside a lifecycle contract feature.
|
||||||
|
|
||||||
|
## Decision 3 - Keep persistence family-local and derived-first
|
||||||
|
|
||||||
|
- **Decision**: Reuse current-table lifecycle and retention anchors per family, and only add family-local hold or deletion-request persistence if v1 can keep that work on the current owning record without widening scope.
|
||||||
|
- **Rationale**: Evidence snapshots and review packs already have `expires_at`; stored reports already have `created_at`, `fingerprint`, `previous_fingerprint`, and an age-based prune command; finding exceptions and decisions already have append-only history plus current aggregate state. A shared contract can be derived from those facts without a new table, and the package should stop at read-only lifecycle truth plus existing download audit if mutation persistence needs shared orchestration.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Central polymorphic lifecycle table: rejected because current repo truth does not justify a second persistence layer.
|
||||||
|
- Purely presentational contract with no family-local persistence option: rejected as too weak if v1 needs auditable hold or deletion-request mutations on current detail surfaces.
|
||||||
|
|
||||||
|
## Decision 4 - Preserve the current suspended-read-only split
|
||||||
|
|
||||||
|
- **Decision**: Keep the repo's current distinction between blocked future generation and preserved retained-history access.
|
||||||
|
- **Rationale**: `ReviewPackService` already blocks new review-pack starts through `WorkspaceCommercialLifecycleResolver`, while `ReviewPackDownloadController` and `ReviewPackDownloadTest` prove that ready-pack downloads remain available when the workspace is suspended read-only. That split is the current release truth and should not be collapsed into retention state.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Treat suspended read-only as retention expiry: rejected because it would contradict Spec 251 and current download behavior.
|
||||||
|
- Hide retained history whenever generation blocks: rejected because it would misstate existing artifact availability.
|
||||||
|
|
||||||
|
## Decision 5 - Keep default proof in unit and feature lanes only
|
||||||
|
|
||||||
|
- **Decision**: Plan default proof in `Unit` and focused `Feature` suites, with browser validation only as an exception.
|
||||||
|
- **Rationale**: The repo already has current feature coverage for evidence, review packs, customer review workspace, tenant review detail, finding-exception workflows, and stored-report pruning. Those suites are the narrowest sufficient proof for this slice.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Add browser smoke by default: rejected because the user asked to keep browser-heavy proof out of the default plan, and the current surfaces already have strong native test seams.
|
||||||
|
|
||||||
|
## Decision 6 - Keep the prep package minimal
|
||||||
|
|
||||||
|
- **Decision**: Create `research.md`, `data-model.md`, and `quickstart.md`, but omit a `contracts/` directory and skip the agent-context update step.
|
||||||
|
- **Rationale**: This slice reuses existing Filament resources, the existing signed `admin.review-packs.download` route, and the existing Laravel stack. A separate OpenAPI or route-contract artifact would duplicate repo truth. The task is also explicitly restricted to `specs/267-artifact-lifecycle-retention/`, so agent-context files outside the spec directory stay untouched.
|
||||||
|
- **Alternatives considered**:
|
||||||
|
- Add a contracts package anyway: rejected because there is no new API surface or route contract.
|
||||||
|
- Run `.specify/scripts/bash/update-agent-context.sh copilot`: rejected for this prep run because it would modify files outside the allowed folder without introducing any new technology.
|
||||||
|
|
||||||
|
## Cross-Spec Alignment
|
||||||
|
|
||||||
|
- **Spec 158** remains the truth-semantics anchor for existing evidence, review, and review-pack outcome surfaces. This slice extends that shared truth path instead of contradicting it.
|
||||||
|
- **Spec 251** remains the commercial lifecycle anchor for suspended read-only behavior. This slice reuses its existing allow or block split and does not turn commercial posture into retention truth.
|
||||||
|
- **Spec 262** remains the taxonomy authority for keeping lifecycle, retention, commercial state, provider presence, and restoreability separate. This slice consumes that taxonomy and does not reopen it.
|
||||||
|
- **Spec 265** remains the decision-register surface spec. This slice extends shared artifact semantics into accepted-risk decision history at the aggregate seam only and does not introduce or rewrite a decision console.
|
||||||
364
specs/267-artifact-lifecycle-retention/spec.md
Normal file
364
specs/267-artifact-lifecycle-retention/spec.md
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
# Feature Specification: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
**Feature Branch**: `267-artifact-lifecycle-retention`
|
||||||
|
**Created**: 2026-05-03
|
||||||
|
**Status**: Ready for implementation
|
||||||
|
**Input**: User description: "Promote the manual-backlog governance artifact lifecycle candidate as the next best remaining target after the auto-prep queue emptied and the decision-register gap moved to Spec 265, while keeping the slice prep-only and tightly bounded to one runtime lifecycle contract for governance artifacts."
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: TenantPilot already stores governance artifacts such as evidence snapshots, stored reports, review packs, and accepted-risk decision history, but each family still exposes local status fragments instead of one honest contract for identity, retention, exportability, hold state, deletion intent, and suspended-read-only access.
|
||||||
|
- **Today's failure**: Operators can tell that an artifact exists, but not reliably whether it is still the current artifact, whether it is retained only as historical evidence, whether it may still be downloaded or exported, whether a hold blocks deletion, or whether suspended-read-only workspace posture preserves safe access. This creates misleading product claims and inconsistent audit expectations.
|
||||||
|
- **User-visible improvement**: Existing artifact detail and download surfaces state one calm truth: what the artifact is, whether it is immutable, what lifecycle role it currently has, what retention state applies, which actions are still allowed, and why a blocked action is blocked.
|
||||||
|
- **Smallest enterprise-capable version**: Add one shared governance-artifact lifecycle contract over existing artifact-owning records, with immutable artifact reference, bounded lifecycle state, bounded retention state, honest export and deletion-request semantics, hold semantics, and suspended-read-only access rules, reused on current artifact surfaces, with hold or deletion-request mutations shipping only where current-owner persistence stays bounded, without introducing a generic artifact super-table, purge engine, or new workflow console.
|
||||||
|
- **Explicit non-goals**: No purge engine implementation, no workspace or tenant closure flows, no billing or subscription truth changes, no support-access governance package, no generic workflow engine, no broad customer portal, no reopening of Spec 262 taxonomy work, no provider-lifecycle expansion beyond existing truth, no dedicated Stored Reports Surface rewrite, and no export-before-deletion bundle workflow in this v1 slice.
|
||||||
|
- **Permanent complexity imported**: One shared artifact reference contract, one bounded artifact lifecycle state family, one bounded artifact retention state family, action and audit semantics reused across existing surfaces, and focused test coverage for lifecycle gating and read-only behavior. No new generic artifact registry UI, no new panel, and no new meta-framework are introduced.
|
||||||
|
- **Why now**: The active auto-prep queue is intentionally empty, `Decision Register & Approval Workflow v1` is already specced as Spec 265, and this candidate is now the next best manual-promotion target in both the backlog priority list and roadmap order. It is the clearest remaining trust and auditability gap on top of already-real evidence and review foundations.
|
||||||
|
- **Why not local**: Review packs, evidence snapshots, stored reports, decision records, download controllers, read-only workspace gating, and audit expectations must all mean the same thing. Local page checks or one-off labels would drift immediately and recreate the current ambiguity.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: New state axis, lifecycle-themed cross-surface contract, and multi-surface adoption. Defense: the scope is intentionally limited to one runtime contract over already-existing artifact records and current surfaces, with explicit follow-up slices for portal, export-before-delete, purge, closure, and support-access work.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: workspace + tenant + canonical-view
|
||||||
|
- **Primary Routes**:
|
||||||
|
- existing tenant-scoped evidence snapshot list and detail surfaces under `/admin/t/{tenant}/evidence` and `/admin/t/{tenant}/evidence/{record}`
|
||||||
|
- existing tenant-scoped review list and detail surfaces under `/admin/t/{tenant}/reviews` and `/admin/t/{tenant}/reviews/{record}` where the current export or retained artifact is explained
|
||||||
|
- existing tenant-scoped review-pack list and detail surfaces under `/admin/t/{tenant}/review-packs` and `/admin/t/{tenant}/review-packs/{record}`
|
||||||
|
- existing signed review-pack download route `admin.review-packs.download`
|
||||||
|
- existing customer review workspace surface under `/admin/reviews/workspace` when retained artifacts are consumed during suspended-read-only posture
|
||||||
|
- no new standalone stored-report or decision-register route is introduced in v1; stored reports and accepted-risk decision history consume the contract headlessly in this slice
|
||||||
|
- **Data Ownership**:
|
||||||
|
- the lifecycle contract applies primarily to existing tenant-owned governance artifact records, including evidence snapshots, stored reports, review packs, and accepted-risk or decision history records
|
||||||
|
- tenant reviews remain the current review-owned context surface that points to retained artifacts; this spec does not redefine tenant-review publication or supersede semantics beyond how retained artifacts are presented there
|
||||||
|
- workspace-owned canonical views may display artifact lifecycle truth but do not become the owning source of that truth
|
||||||
|
- this spec does not create a new product table or generic artifact super-entity
|
||||||
|
- **RBAC**:
|
||||||
|
- workspace entitlement and tenant entitlement remain mandatory before any artifact record, summary, lifecycle badge, or download affordance is revealed
|
||||||
|
- non-members or wrong-scope actors remain deny-as-not-found (`404`)
|
||||||
|
- in-scope members missing the relevant capability remain forbidden (`403`)
|
||||||
|
- any destructive-like lifecycle action shipped in this slice, such as deletion request, hold release, or irreversible expiration, still requires explicit confirmation and server-side authorization
|
||||||
|
- suspended-read-only posture may preserve read access for authorized actors but does not bypass normal scope or capability checks
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: Canonical review-workspace and retained-artifact consumption surfaces open prefiltered to the current tenant when launched from tenant context. The filter is convenience only and must not widen back to other tenants implicitly.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Lifecycle state, retention state, download availability, hold markers, and deletion-request indicators on canonical surfaces must be derived only from artifact rows that belong to entitled tenants inside the current workspace. Signed downloads and linked detail views must re-check tenant entitlement at request time.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes
|
||||||
|
- **Interaction class(es)**: evidence and report viewers, status messaging, lifecycle badges, detail summaries, download actions, destructive-like lifecycle actions, and audit-linked artifact navigation
|
||||||
|
- **Systems touched**: existing evidence snapshot resource, tenant review resource, review-pack resource, review-pack download controller, customer review workspace, shared badge rendering, shared artifact truth presentation, existing workspace commercial lifecycle overlay, and workspace audit logging
|
||||||
|
- **Existing pattern(s) to extend**: existing governance artifact truth summaries on evidence, review, and review-pack surfaces; existing workspace suspended-read-only gating; existing audit logging for review-pack downloads and review lifecycle mutations
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: shared artifact-truth presentation, centralized badge catalog and badge renderer, existing capability and policy enforcement, workspace audit logger with stable action IDs, and the current commercial lifecycle resolver for suspended-read-only behavior
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: the current shared artifact-truth path already explains execution and readiness truth for evidence, reviews, and review packs. It is insufficient for immutable identity, retention state, hold and delete-request semantics, and the difference between historical readability and active exportability. This spec extends that contract instead of creating a second lifecycle language.
|
||||||
|
- **Allowed deviation and why**: none. Stored-report adoption and accepted-risk decision-history adoption stay headless in v1, but they must still consume this same contract rather than inventing a parallel local model.
|
||||||
|
- **Consistency impact**: `current`, `historical`, `superseded`, `retained`, `on hold`, `deletion requested`, `expired access`, `download allowed`, and `blocked in suspended-read-only` must each keep one meaning across evidence, review-pack, stored-report, and decision-record contexts.
|
||||||
|
- **Review focus**: reviewers must verify that the implementation extends the shared artifact-truth and audit path, does not add page-local lifecycle taxonomies, and does not bypass existing read-only gating or download authorization.
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: yes, at the contract boundary only
|
||||||
|
- **Shared OperationRun UX contract/layer reused**: existing shared OperationRun start and completion UX for review-pack generation and any future long-running export or irreversible deletion-class automation
|
||||||
|
- **Delegated start/completion UX behaviors**: existing review-pack generation keeps the shared queued toast, link, artifact-link, and terminal-notification behavior. Any future export bundle creation or irreversible deletion automation must reuse that same shared path rather than local action messaging.
|
||||||
|
- **Local surface-owned behavior that remains**: if a family passes the bounded current-owner persistence gate, its current artifact detail surface remains responsible for reason capture, current-state disclosure, and local confirmation messaging for direct lifecycle mutations such as placing or releasing a hold or requesting deletion.
|
||||||
|
- **Queued DB-notification policy**: explicit opt-in only for future long-running export or deletion-class flows; no terminal DB notification is introduced for direct hold or deletion-request capture on any family that ships inside the bounded gate.
|
||||||
|
- **Terminal notification path**: central lifecycle mechanism for any future async export or deletion run; `N/A` for bounded direct mutations that remain synchronous in this slice.
|
||||||
|
- **Exception required?**: none
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Boundary classification**: platform-core
|
||||||
|
- **Seams affected**: governance artifact identity, lifecycle vocabulary, retention vocabulary, export and delete semantics, and suspended-read-only artifact consumption
|
||||||
|
- **Neutral platform terms preserved or introduced**: `governance artifact`, `artifact reference`, `lifecycle state`, `retention state`, `historical artifact`, `hold`, `deletion request`, `download allowed`, and `retained history`
|
||||||
|
- **Provider-specific semantics retained and why**: provider freshness, evidence completeness, and provider-derived content stay inside the existing artifact truth summaries. This spec does not assign provider object lifecycle meaning to governance artifacts.
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: the contract governs TenantPilot-owned artifacts and their local lifecycle. It explicitly forbids using provider presence, provider deletion, or provider capability limits as a proxy for artifact retention or deletion state.
|
||||||
|
- **Follow-up path**: `follow-up-spec` for any provider-lifecycle expansion beyond current artifact truth
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot list and detail | yes | Native Filament resource plus shared artifact-truth entry | evidence viewers, status messaging, lifecycle badges | detail header, list summary, related-context links | no | Existing `Evidence` resource keeps its shape; the lifecycle contract changes truth and allowed-action disclosure only |
|
||||||
|
| Tenant review detail and current export summary | yes | Native Filament resource plus shared artifact-truth entry | evidence/report viewers, status messaging | detail summary, related artifact references | no | Review detail stays the anchor context for review-derived artifacts; this spec does not add a new review workflow page |
|
||||||
|
| Review-pack registry, detail, and signed download | yes | Native Filament resource plus existing signed download controller | report viewers, download actions, lifecycle badges, destructive-like action copy | list summary, detail header, download gate | no | Existing `Review Packs` resource remains the primary retained-artifact surface |
|
||||||
|
| Customer review workspace retained-artifact consumption | yes | Native Filament page with existing linked detail surfaces | canonical navigation, read-only explanations, report viewers | canonical table, linked detail, read-only explanation | no | The page remains read-only and scan-first; v1 only clarifies retained-artifact truth during suspended-read-only posture |
|
||||||
|
| Stored-report and accepted-risk record browsing surfaces | no | N/A | none in v1 | none in v1 | no | Contract adoption is in scope, but stored reports stay headless and current Spec 265 browsing surfaces stay unchanged until later specs |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot list and detail | Secondary Context Surface | Operator decides whether evidence remains current, historical, held, or deletion-requested before taking downstream review or export steps | Artifact reference, lifecycle state, retention state, next allowed action, read-only or suspension note | Raw completeness reasons, operation context, dimension details | Not primary because evidence lifecycle must stay attached to the artifact being inspected, not a new global inbox | Follows existing evidence inspection workflow | Prevents operators from reconstructing lifecycle from separate status, expiry, and action states |
|
||||||
|
| Tenant review detail and current export summary | Secondary Context Surface | Operator decides whether the current review still points to an exportable retained artifact or only to historical evidence | Current export reference, lifecycle state, retention state, blocked or allowed next action | Publish blockers, related evidence, operation details | Not primary because the review detail already owns the review decision context | Keeps artifact lifecycle inside existing review navigation | Avoids jumping between review detail and review-pack detail just to understand retention truth |
|
||||||
|
| Review-pack registry, detail, and signed download | Secondary Context Surface | Operator decides whether an existing pack may be downloaded, held, or marked for deletion | Artifact reference, lifecycle state, retention state, download allowed or blocked, suspension explanation | Evidence basis, SHA, operation link, related review | Not primary because the pack lifecycle should be understood in the artifact context itself | Stays inside reporting workflow instead of creating a second artifact-management console | Removes guesswork about whether `ready` still means accessible or retained |
|
||||||
|
| Customer review workspace retained-artifact consumption | Tertiary Evidence / Diagnostics Surface | Customer-safe or operator read-only consumer verifies what history is still safely readable while a workspace is suspended read-only | Retained artifact availability and one calm read-only explanation | Raw evidence, support-only diagnostics, and lower-level lifecycle details stay linked or gated | Not primary because it answers `what remains readable now` rather than `what lifecycle change should happen` | Preserves evidence-first review consumption instead of forcing a portal rewrite | Prevents suspended workspaces from looking fully unavailable when retained history is intentionally preserved |
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot list and detail | operator-MSP, support-platform | Artifact reference, lifecycle state, retention state, next allowed action | Completeness state, expiry basis, related operation, downstream review-pack reference | Raw evidence dimension payloads remain secondary | `Refresh evidence` when allowed, otherwise `View related artifact` | Raw payload JSON and low-level provenance stay hidden or support-gated | Lifecycle truth is stated once in the summary, while later sections add supporting evidence only |
|
||||||
|
| Tenant review detail and current export summary | operator-MSP, support-platform | Current export reference, lifecycle and retention summary, blocked or allowed next action | Publish blockers, evidence linkage, related pack and operation links | Support-only raw details remain hidden outside support context | `Open current export` or `View evidence` | Raw fingerprints and low-level interpretation details stay hidden outside support mode | Review status does not repeat retention truth; each section adds different information |
|
||||||
|
| Review-pack registry, detail, and signed download | customer-read-only, operator-MSP, support-platform | Artifact reference, lifecycle state, retention state, download allowed or blocked, and read-only explanation when relevant | Evidence basis, generation outcome, expiry reason, related review and operation | SHA, fingerprints, and support-only details remain gated | `Download current pack` when allowed, otherwise `View source review` | Support-only diagnostics and lifecycle mutation reasons stay collapsed or gated | Pack status, retention state, and suspension reason are shown once and not re-labeled differently later |
|
||||||
|
| Customer review workspace retained-artifact consumption | customer-read-only, operator-MSP | Available retained artifacts, whether the workspace is read-only, and why current mutations are blocked | Linked review or pack history and current artifact status | Support or raw diagnostic detail remains off the surface | `Open current review` or `Open current pack` | Any destructive or admin lifecycle controls stay hidden | The page gives one calm availability explanation and delegates deeper detail to the linked artifact pages |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot list and detail | List / Table / Detail | Read-only registry report | Open current evidence or inspect lifecycle state | Clickable row to detail | required | Existing safe navigation and related links stay secondary | `More` group or detail danger area with confirmation for destructive-like lifecycle actions | `/admin/t/{tenant}/evidence` | `/admin/t/{tenant}/evidence/{record}` | Tenant context, snapshot reference, suspension note where relevant | Evidence snapshot | Reference, lifecycle state, retention state, next action | none |
|
||||||
|
| Tenant review detail and current export summary | Detail / Report viewer | Read-only detail with retained-artifact context | Open the current retained pack or evidence basis | Detail page with linked retained artifact | allowed from list | Supporting navigation and related links stay secondary | Existing archive remains where it already lives; any new lifecycle actions on child artifacts stay off the review detail primary plane | `/admin/t/{tenant}/reviews` | `/admin/t/{tenant}/reviews/{record}` | Tenant context, review reference, linked artifact reference | Review | Current artifact availability and lifecycle truth | none |
|
||||||
|
| Review-pack registry, detail, and signed download | List / Table / Detail / Download | Read-only registry report | Open or download the current pack if retained | Clickable row to detail, then explicit download | required | Existing `Download` shortcut and linked evidence or review navigation remain secondary | `More` group or detail danger area with confirmation for deletion request or hold release | `/admin/t/{tenant}/review-packs` | `/admin/t/{tenant}/review-packs/{record}` plus `admin.review-packs.download` | Tenant context, pack reference, linked review, suspended-read-only explanation | Review pack | Reference, lifecycle state, retention state, download allowed or blocked | none |
|
||||||
|
| Customer review workspace retained-artifact consumption | Canonical / Table / Linked detail | Read-only registry report | Open the current retained review or pack | Primary link column to existing detail pages | forbidden for non-linked rows | Clear filters and supporting navigation stay secondary | none on the workspace page | `/admin/reviews/workspace` | Existing linked detail pages under review, evidence, or pack resources | Workspace context, current tenant filter, read-only explanation | Customer review artifact | What remains readable now and what is blocked now | Dedicated canonical-surface exception because the page links outward instead of owning inline detail |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot list and detail | Tenant operator | Decide whether this evidence artifact remains the current retained basis for review and export | List/detail | Is this evidence artifact still current, retained, and safe to use? | Artifact reference, lifecycle state, retention state, next allowed action | Raw completeness payloads and low-level provenance | artifact lifecycle, retention state, completeness, suspended-read-only availability | TenantPilot artifact truth only | Refresh evidence, open related review or pack | Request deletion, place or release hold, expire snapshot |
|
||||||
|
| Tenant review detail and current export summary | Review owner | Decide whether the current retained artifact is still the right review output to share or preserve | Detail | Which retained artifact does this review currently point to, and is it still available? | Current export reference, lifecycle state, retention state, blocked or allowed next action | Publish blockers, related evidence details, low-level interpretation context | review lifecycle context, artifact lifecycle, retention state | TenantPilot artifact truth only | Open current export, open evidence snapshot | Existing review archive only; child-artifact lifecycle changes happen on the artifact surface |
|
||||||
|
| Review-pack registry, detail, and signed download | Reporting operator or customer-safe consumer | Decide whether an already-generated pack may be downloaded now or only preserved as historical evidence | List/detail/download | Can I still use this pack, and what does its lifecycle state mean? | Artifact reference, lifecycle state, retention state, download allowed or blocked, read-only explanation | Evidence basis, generation diagnostics, SHA, operation link | artifact lifecycle, retention state, generation outcome, suspended-read-only availability | TenantPilot artifact truth only | Download current pack, view source review | Request deletion, place or release hold |
|
||||||
|
| Customer review workspace retained-artifact consumption | Customer-safe reader | Decide what retained history remains readable while the workspace is suspended read-only | Canonical table and linked detail | What history can I still read right now? | Available retained artifacts and one calm read-only explanation | Raw or support-only diagnostics remain linked or hidden | retained availability, workspace read-only posture | TenantPilot read-only only | Open current review, open current pack | none |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: yes - one governance-artifact lifecycle contract over existing artifact records
|
||||||
|
- **New persisted entity/table/artifact?**: no generic new entity or table by default
|
||||||
|
- **New abstraction?**: no new generic engine; one bounded shared contract only
|
||||||
|
- **New enum/state/reason family?**: yes - one bounded lifecycle state family and one bounded retention state family for governance artifacts
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no new UI framework; only a bounded lifecycle vocabulary reused in existing surfaces
|
||||||
|
- **Current operator problem**: operators cannot currently answer whether a governance artifact is current, historical, held, deletion-requested, still downloadable, or merely blocked by suspended-read-only posture without decoding multiple local statuses.
|
||||||
|
- **Existing structure is insufficient because**: review packs, evidence snapshots, stored reports, and decision history each expose different partial status clues. None of those local clues alone answers identity, retention, or allowed-action truth.
|
||||||
|
- **Narrowest correct implementation**: reuse existing artifact-owning records and shared artifact-truth surfaces, add one shared lifecycle and retention contract, and keep destructive or long-running follow-up mechanics out of scope.
|
||||||
|
- **Ownership cost**: shared vocabulary, focused policy and surface tests, a small amount of lifecycle-specific audit data, and reviewer discipline around action semantics.
|
||||||
|
- **Alternative intentionally rejected**: a generic artifact registry, workflow engine, portal, or purge framework was rejected as too broad. Local page-only fixes were rejected because they would preserve cross-surface drift.
|
||||||
|
- **Release truth**: current-release truth grounded in already persisted governance artifacts and already-real read-only suspension behavior, not speculative future platform preparation
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, migration shims, historical aliases, and compatibility-only test coverage are out of scope unless a later implementation slice proves they are necessary.
|
||||||
|
|
||||||
|
Canonical replacement of ambiguous lifecycle wording is preferred over preserving overloaded terminology.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Feature
|
||||||
|
- **Validation lane(s)**: fast-feedback + confidence
|
||||||
|
- **Why this classification and these lanes are sufficient**: the bounded implementation slice proves behavior through model and policy invariants, controller download authorization, and Livewire or Filament surface tests on current artifact pages. Browser coverage is not required unless lifecycle gating proves unstable only in the real browser.
|
||||||
|
- **New or expanded test families**: focused artifact-lifecycle feature coverage for review packs, evidence snapshots, suspended-read-only access, headless stored-report truth, and aggregate-level accepted-risk decision-history behavior
|
||||||
|
- **Fixture / helper cost impact**: moderate; tests need workspace, tenant, membership, and a small curated artifact matrix, but they do not need a new heavy governance harness or provider-wide setup
|
||||||
|
- **Heavy-family visibility / justification**: none by default. If a later implementation adds a broad cross-artifact matrix, it must be named explicitly as heavy governance rather than hidden inside ordinary Filament tests.
|
||||||
|
- **Special surface test profile**: shared-detail-family
|
||||||
|
- **Standard-native relief or required special coverage**: standard native Filament coverage is sufficient for evidence, review, and review-pack surfaces; controller download tests and policy tests cover the signed download and read-only gate behavior
|
||||||
|
- **Reviewer handoff**: reviewers must confirm that lifecycle truth stays centralized, direct mutations stay audit-backed only when the bounded current-owner gate passes, accepted-risk decision adoption stays headless in this slice, no generic artifact engine appears, and the exact proof commands stay limited to the canonical artifact-lifecycle test suite plus Pint hygiene
|
||||||
|
- **Budget / baseline / trend impact**: minor expected drift from a new cross-artifact test matrix; no browser or broad heavy-family expansion should appear in v1
|
||||||
|
- **Escalation needed**: none
|
||||||
|
- **Active feature PR close-out entry**: Guardrail
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/GovernanceArtifactTruth/GovernanceArtifactLifecycleContractTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewLifecycleTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingExceptionRevocationTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/StoredReportModelTest.php tests/Feature/PermissionPosture/PruneStoredReportsCommandTest.php tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Understand what an artifact is allowed to do now (Priority: P1)
|
||||||
|
|
||||||
|
As a reporting or review operator, I want every retained governance artifact to show one stable reference, one lifecycle role, one retention state, and one honest next action so that I do not have to infer download, hold, or delete meaning from multiple local statuses.
|
||||||
|
|
||||||
|
**Why this priority**: This is the core operator trust problem. If the artifact surface still leaves lifecycle truth ambiguous, the spec delivers no real product value.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by opening an evidence snapshot, a review pack, and a review-derived artifact summary in different lifecycle states and confirming that each surface answers identity, lifecycle, retention, and next action from the first screen.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a retained review pack that is still downloadable, **When** the operator opens the pack detail, **Then** the surface shows a stable artifact reference, its lifecycle state, its retention state, and that download is currently allowed.
|
||||||
|
2. **Given** a historical evidence snapshot that is no longer the current evidence basis, **When** the operator opens it, **Then** the surface states that it is historical rather than current and does not imply that deletion or purge already happened.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Preserve history during suspended read-only posture (Priority: P1)
|
||||||
|
|
||||||
|
As an authorized customer-safe or operator read-only consumer, I want suspended-read-only posture to preserve retained history without implying new artifact generation or hidden deletion, so that I can still inspect evidence and downloads that the product promises to retain.
|
||||||
|
|
||||||
|
**Why this priority**: The contract must stay honest at the moment when commercial suspension or workspace freeze would otherwise make artifacts look unavailable or deceptively mutable.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by placing a workspace into suspended-read-only posture and verifying that retained artifacts remain viewable and already-generated downloads remain governed by retention truth, while tenant-plane mutation or generation actions are blocked.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a suspended-read-only workspace with a retained review pack, **When** an authorized consumer opens the current pack or review workspace, **Then** the product still exposes the retained artifact and explains that mutation is blocked because the workspace is read-only.
|
||||||
|
2. **Given** a suspended-read-only workspace, **When** a tenant-plane actor attempts a new artifact generation or lifecycle mutation, **Then** the surface blocks the action with one consistent read-only explanation instead of hiding retained history.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Audit every lifecycle-sensitive artifact action (Priority: P2)
|
||||||
|
|
||||||
|
As a workspace or compliance operator, I want download actions and any bounded current-owner lifecycle mutations on governance artifacts to leave a stable audit trail tied to the artifact reference and scope, so that future review or customer conversations can prove what happened to an artifact and why.
|
||||||
|
|
||||||
|
**Why this priority**: Artifact lifecycle changes without audit proof would undermine the trust value this spec is meant to add.
|
||||||
|
|
||||||
|
**Independent Test**: Can be fully tested by triggering a retained-artifact download and, only on artifact families that pass the bounded current-owner persistence gate, placing a hold and requesting deletion while verifying that each shipped action records the artifact reference, actor, scope, originating surface, and before or after state.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an operator downloads a retained review pack, **When** the controller serves the file, **Then** the audit trail records the artifact reference, actor, tenant, workspace, and source surface.
|
||||||
|
2. **Given** an artifact family whose hold or deletion-request persistence stays on the current owning record, **When** an operator places a hold or requests deletion, **Then** the mutation records the reason and the before or after retention state without claiming the content was already purged.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- A review pack is `ready` but the workspace is suspended read-only; the contract must distinguish `download still allowed` from `new generation blocked`.
|
||||||
|
- An evidence snapshot is expired from direct access but remains historically referenced by a published review; the contract must not erase its immutable reference or imply that review history is invalid.
|
||||||
|
- A stored report has a stable fingerprint but no dedicated browsing surface yet; the contract must still define its lifecycle and retention expectations without forcing a new UI surface into v1.
|
||||||
|
- An accepted-risk decision record is append-only and later superseded by a newer decision; the older decision must remain historically addressable rather than looking editable or deleted.
|
||||||
|
- A deletion request is placed on an artifact that is also on hold; the hold must win and the UI must explain that the request is blocked from progressing.
|
||||||
|
- Canonical review-workspace tables must not leak that another tenant has a held, deletion-requested, or downloadable artifact outside the viewer's current entitlement.
|
||||||
|
|
||||||
|
## Requirements *(mandatory)*
|
||||||
|
|
||||||
|
**Constitution alignment (required):** This feature introduces no new Microsoft Graph call path. It governs TenantPilot-owned governance artifacts and their local lifecycle truth only. Existing safety gates, tenant isolation, and audit expectations remain mandatory.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** This feature introduces one bounded lifecycle contract and one bounded retention contract because the current operator workflow already needs them across multiple real artifact families. It must not create a generic artifact engine, a super-table, or speculative portal infrastructure.
|
||||||
|
|
||||||
|
**Constitution alignment (XCUT-001):** The feature extends existing artifact-truth, badge, read-only gating, and audit paths. No page-local lifecycle language or one-off viewer logic is allowed.
|
||||||
|
|
||||||
|
**Constitution alignment (DECIDE-AUD-001 / OPSURF-001):** Every affected surface must separate customer-readable lifecycle truth, operator diagnostics, and support-only detail. One dominant next action must stay primary, and the same lifecycle fact must not be restated differently in multiple sections.
|
||||||
|
|
||||||
|
**Constitution alignment (PROV-001):** Provider-derived evidence remains inside the existing artifact truth summaries. Provider presence or provider deletion must not become a proxy for governance-artifact retention or deletion state.
|
||||||
|
|
||||||
|
**Constitution alignment (TEST-GOV-001):** The implementation must stay in focused feature coverage. No hidden heavy-governance or browser family may appear by default.
|
||||||
|
|
||||||
|
**Constitution alignment (OPS-UX / OPS-UX-START-001):** Existing review-pack generation and any future long-running export or irreversible deletion run must reuse the shared OperationRun UX path. If a family passes the bounded current-owner persistence gate, direct hold placement, hold release, or deletion request capture may remain direct audit-backed mutations and must not silently grow their own async notification model.
|
||||||
|
|
||||||
|
**Constitution alignment (RBAC-UX):** Authorization planes remain unchanged. Non-member or wrong-scope access returns `404`, in-scope capability denial returns `403`, server-side policies remain authoritative, and destructive-like lifecycle actions require confirmation.
|
||||||
|
|
||||||
|
**Constitution alignment (BADGE-001):** Lifecycle and retention badges must stay centralized and must not introduce page-local color or wording maps.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-FIL-001 / UX-001):** Existing Filament resources and pages remain the primary surfaces. The implementation must reuse native Filament components and shared primitives, avoid ad-hoc styling, preserve one dominant primary action per surface, keep detail disclosure in infolists or native sections, and avoid adding a new artifact-management design system.
|
||||||
|
|
||||||
|
**Constitution alignment (UI-NAMING-001):** Primary operator-facing labels use `artifact`, `evidence snapshot`, `review pack`, `stored report`, `accepted-risk record`, `download`, `place hold`, `release hold`, and `request deletion`. Terms such as `soft delete`, `hard delete`, or `purge executor` stay implementation-level and must not be the primary operator wording in v1.
|
||||||
|
|
||||||
|
**Constitution alignment (Filament Action Surfaces):** The Action Surface Contract remains satisfied when evidence, review, and review-pack surfaces keep their current inspect models, add no redundant View action, keep navigation separate from mutation, and group destructive-like lifecycle actions under the existing danger or `More` placement rules.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-267-001**: The system MUST define one shared governance-artifact lifecycle contract for evidence snapshots, stored reports, review packs, and accepted-risk or decision records.
|
||||||
|
- **FR-267-002**: The shared contract MUST expose an immutable artifact reference that remains stable across superseding, holds, deletion requests, suspended-read-only access, and historical retention.
|
||||||
|
- **FR-267-003**: The contract MUST separate artifact lifecycle state from artifact retention state.
|
||||||
|
- **FR-267-004**: The artifact lifecycle state MUST answer whether an artifact is current, historical, superseded, or otherwise removed from active circulation without implying purge or provider deletion.
|
||||||
|
- **FR-267-005**: The artifact retention state MUST answer whether an artifact is retained, on hold, deletion-requested, or expired from direct access, without collapsing those meanings into workspace suspension, review publication status, or provider truth.
|
||||||
|
- **FR-267-006**: The contract MUST distinguish direct download or export of an already-generated artifact from a future export-before-deletion workflow.
|
||||||
|
- **FR-267-007**: When v1 includes a hold mutation for a concrete artifact family, that hold MUST preserve the artifact reference, prevent deletion progression, and remain visible on the artifact surface until explicitly released.
|
||||||
|
- **FR-267-008**: When v1 includes a deletion-request mutation for a concrete artifact family, that request MUST be explicit, auditable, and reversible until a later deletion or purge follow-up executes. In v1 it MUST NOT claim that artifact content was already destroyed.
|
||||||
|
- **FR-267-009**: The first implementation slice MUST treat `deletion requested` only as reversible removal from normal operator circulation while immutable reference and audit history remain intact. Irreversible hard-delete semantics remain reserved for a later purge or closure slice.
|
||||||
|
- **FR-267-010**: Suspended-read-only workspace posture MUST preserve authorized read access to retained artifacts and already-generated downloads when their retention state allows it, while blocking tenant-plane generation or lifecycle mutations that would change artifact truth.
|
||||||
|
- **FR-267-011**: Existing evidence, review, and review-pack detail surfaces MUST present artifact reference, lifecycle state, retention state, and the next allowed or blocked action without requiring the operator to decode multiple local status fields.
|
||||||
|
- **FR-267-012**: Existing canonical customer-review consumption surfaces MUST show one calm read-only explanation and must not imply that retained artifacts vanished simply because the workspace is suspended read-only.
|
||||||
|
- **FR-267-013**: Stored reports and accepted-risk or decision records MUST adopt the same lifecycle and retention contract even if stored reports stay headless and the current decision register or detail surfaces remain unchanged in this slice.
|
||||||
|
- **FR-267-014**: Accepted-risk or decision history records MUST remain append-only and historically addressable even when a newer decision supersedes them.
|
||||||
|
- **FR-267-015**: Every in-scope download, and every hold, hold release, or deletion-request mutation that passes the bounded current-owner persistence gate, MUST write an audit event that records the artifact reference, actor, workspace, tenant when present, originating surface, and before or after lifecycle state as appropriate.
|
||||||
|
- **FR-267-016**: The first implementation slice MUST reuse existing capability and policy enforcement and MUST include both positive and negative authorization coverage for detail visibility, download access, and destructive-like lifecycle mutations.
|
||||||
|
- **FR-267-017**: The feature MUST NOT create a generic artifact registry UI, a generic workflow engine, a broad customer artifact portal, or a purge automation engine.
|
||||||
|
- **FR-267-018**: The feature MUST explicitly defer dedicated Stored Reports Surface work, Workspace and Tenant Closure Lifecycle work, Data Export Before Deletion workflow, Retention and Purge Governance, and Enterprise Access Boundary or Support Access Governance to named follow-up specs.
|
||||||
|
- **FR-267-019**: The feature MUST consume Spec 262 as closed taxonomy input and MUST NOT reopen or normalize it.
|
||||||
|
- **FR-267-020**: The feature MUST treat Spec 158 as context for truthful artifact semantics and MUST extend, not contradict, that product-truth direction.
|
||||||
|
|
||||||
|
## UI Action Matrix *(mandatory when Filament is changed)*
|
||||||
|
|
||||||
|
| Surface | Location | Header Actions | Inspect Affordance (List/Table) | Row Actions (max 2 visible) | Bulk Actions (grouped) | Empty-State CTA(s) | View Header Actions | Create/Edit Save+Cancel | Audit log? | Notes / Exemptions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Evidence snapshot resource | Existing `EvidenceSnapshotResource` list and view surfaces | Keep existing `Create snapshot`; no new lifecycle CTA on the list header in v1 | Existing `recordUrl()` clickable row remains primary | Keep current safe row actions; no more than one direct lifecycle shortcut | none | Existing `Create snapshot` empty-state CTA remains | Keep `Refresh evidence` primary; any later gated `Place hold`, `Release hold`, or `Request deletion` stays in `More` or the danger area with confirmation only if the family passes the bounded persistence gate | N/A | yes | Action Surface Contract stays satisfied; no redundant View action and no new bulk family |
|
||||||
|
| Tenant review resource | Existing `TenantReviewResource` detail surface and current export summary | Existing header actions remain; no new top-level review-lifecycle family is added by this spec | Existing clickable-row review inspection remains primary | Existing inline export shortcut remains the only direct row shortcut | none | Existing `Create first review` remains | Keep current review actions; child-artifact lifecycle changes stay on the artifact surface rather than the review primary plane | N/A | yes | This spec changes summary truth and linked artifact context, not the core review workflow |
|
||||||
|
| Review-pack resource and signed download | Existing `ReviewPackResource` list and view surfaces plus `admin.review-packs.download` | Keep existing `Generate pack` entry points outside this spec's new lifecycle family | Existing clickable row remains primary | Keep `Download` as the only direct safe shortcut | none | Existing generate CTA remains while the list is empty | Keep `Download` and `Regenerate`; any later gated `Place hold`, `Release hold`, or `Request deletion` is grouped under `More` or visible danger placement with confirmation only if the family passes the bounded persistence gate | N/A | yes | Signed download keeps server-side entitlement checks and audit logging; no new artifact portal route |
|
||||||
|
| Customer review workspace | Existing `CustomerReviewWorkspace` page | Keep only `Clear filters` | Existing primary link column remains the inspect model | none | none | Existing `Clear filters` empty-state CTA when filtered | N/A | N/A | existing open and download audits remain | No destructive actions appear on the canonical workspace page in v1 |
|
||||||
|
|
||||||
|
### Key Entities *(include if feature involves data)*
|
||||||
|
|
||||||
|
- **Governance Artifact Reference**: Stable identity for one retained governance artifact, including its family, owning scope, stable record identity, and any immutable content fingerprint or historical anchor needed to distinguish it from later artifacts.
|
||||||
|
- **Governance Artifact Lifecycle State**: The contract that answers whether the artifact is current, historical, superseded, or otherwise removed from active circulation without implying retention or purge truth.
|
||||||
|
- **Governance Artifact Retention State**: The contract that answers whether the artifact is retained, on hold, deletion-requested, or expired from direct access.
|
||||||
|
- **Governance Artifact Lifecycle Event**: The auditable action record produced when an artifact is downloaded, or when a bounded current-owner mutation path places it on hold, releases a hold, or marks it for deletion.
|
||||||
|
- **Accepted-Risk or Decision Record**: Append-only governance history entry that documents risk acceptance or decision outcomes and must remain historically addressable even when later decisions supersede it.
|
||||||
|
|
||||||
|
## Success Criteria *(mandatory)*
|
||||||
|
|
||||||
|
### Measurable Outcomes
|
||||||
|
|
||||||
|
- **SC-267-001**: In the first implementation slice, 100% of in-scope artifact detail or download surfaces show immutable reference, lifecycle state, retention state, and next allowed or blocked action in one inspection step.
|
||||||
|
- **SC-267-002**: In a curated review set of 12 artifact cases across evidence, review-pack, and review-derived contexts, operators correctly answer whether the artifact is current, historical, held, deletion-requested, or downloadable in at least 11 of 12 cases without opening raw diagnostics.
|
||||||
|
- **SC-267-003**: In suspended-read-only validation coverage, 100% of retained artifacts that should remain readable stay accessible to authorized users, and 0 blocked tenant-plane mutation or generation actions appear as allowed.
|
||||||
|
- **SC-267-004**: In focused audit regression coverage, 100% of in-scope download actions, and 100% of any hold, hold-release, or deletion-request mutations that pass the bounded current-owner persistence gate, produce an artifact-reference-based audit trail.
|
||||||
|
- **SC-267-005**: No in-scope surface uses provider lifecycle, workspace suspension, or review publication state as a proxy for artifact retention truth.
|
||||||
|
- **SC-267-006**: If no artifact family can add hold or deletion-request persistence without widening scope, the implementation still counts as successful once read-only lifecycle truth, existing download audit, and the explicit mutation split decision are all proven.
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- Spec 262 is completed and remains the closed taxonomy authority. This spec consumes it as context only.
|
||||||
|
- Spec 158 remains the earlier artifact-truth foundation and is used as context only.
|
||||||
|
- Suspended-read-only workspace behavior already exists through the commercial lifecycle overlay and is reused rather than redesigned here.
|
||||||
|
- Review-pack downloads already have server-side entitlement and audit behavior that this contract extends.
|
||||||
|
- Stored reports and accepted-risk decision records already exist as persisted governance artifacts even though their dedicated browsing surfaces are still incomplete or deferred.
|
||||||
|
- Spec 265 now owns the broader decision-register and approval workflow gap; this spec only covers artifact lifecycle semantics for accepted-risk or decision records through shared contract mapping and aggregate-level history truth.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- `docs/product/spec-candidates.md`
|
||||||
|
- `docs/product/roadmap.md`
|
||||||
|
- `docs/product/standards/lifecycle-governance.md`
|
||||||
|
- `specs/158-artifact-truth-semantics/spec.md`
|
||||||
|
- `specs/251-commercial-entitlements-billing-state/spec.md`
|
||||||
|
- `specs/262-lifecycle-governance-taxonomy/spec.md`
|
||||||
|
- `specs/265-decision-register-approval/spec.md`
|
||||||
|
- existing evidence, review, review-pack, and accepted-risk runtime seams, including `StoredReport`, `ReviewPack`, `TenantReview`, `FindingExceptionDecision`, `ReviewPackService`, `TenantReviewLifecycleService`, `ReviewPackResource`, `TenantReviewResource`, and `ReviewPackDownloadController`
|
||||||
|
|
||||||
|
## Non-Goals
|
||||||
|
|
||||||
|
- Implementing a purge engine, retention scheduler, or irreversible deletion executor
|
||||||
|
- Implementing workspace or tenant closure flows
|
||||||
|
- Reworking billing, subscription, or commercial-state truth
|
||||||
|
- Implementing support-access governance, delegated access, or impersonation controls
|
||||||
|
- Creating a generic artifact registry service, workflow engine, or customer artifact portal
|
||||||
|
- Reopening or normalizing Spec 262 taxonomy work
|
||||||
|
- Expanding provider-lifecycle semantics beyond current artifact truth
|
||||||
|
- Delivering the dedicated Stored Reports Surface, Data Export Before Deletion workflow, or broad retention-governance console in this v1 slice
|
||||||
|
|
||||||
|
## Candidate Selection Rationale
|
||||||
|
|
||||||
|
- **Selected candidate**: Governance Artifact Lifecycle & Retention v1
|
||||||
|
- **Source locations**:
|
||||||
|
- `docs/product/spec-candidates.md` manual-promotion backlog priority 2
|
||||||
|
- `docs/product/roadmap.md` productization order 3
|
||||||
|
- `docs/product/standards/lifecycle-governance.md`
|
||||||
|
- **Why selected now**: the active auto-prep queue is intentionally empty, Spec 265 already removes the decision-register candidate from eligibility, and this is the next best remaining manual-promotion target with clear repo-real anchors.
|
||||||
|
- **Completed-spec guardrail result**: Spec 262 is treated as completed closed context only and is not reopened. Spec 158 is treated as context only. Spec 265 makes the earlier decision-register gap ineligible for this slot.
|
||||||
|
- **Smallest viable implementation slice**: apply the shared lifecycle and retention contract to current evidence, review-pack, and review-derived artifact surfaces; keep stored reports and accepted-risk records in the contract scope without forcing a new browsing console or current decision-surface rewrite; ship hold or deletion-request mutations only where bounded current-owner persistence remains local.
|
||||||
|
- **Why close alternatives are deferred**:
|
||||||
|
- `Stored Reports Surface v1` is a dedicated product-surface follow-up and should not be hidden inside this lifecycle contract
|
||||||
|
- `Workspace & Tenant Closure Lifecycle v1` is broader commercial and tenant lifecycle work and remains separate from artifact lifecycle
|
||||||
|
- `Data Export Before Deletion v1` is a workflow slice and remains separate from direct download of already-generated artifacts
|
||||||
|
- `Retention & Purge Governance v1` is the dedicated irreversible deletion and purge follow-up
|
||||||
|
- `Enterprise Access Boundary & Support Access Governance v1` is an access-governance slice, not an artifact-lifecycle slice
|
||||||
|
|
||||||
|
## Follow-up Map
|
||||||
|
|
||||||
|
| Follow-up slice | Why it stays separate | Dependency on this spec |
|
||||||
|
|---|---|---|
|
||||||
|
| Stored Reports Surface v1 | Productizes how stored reports are browsed and consumed; it should reuse the lifecycle contract rather than create it | Reuses immutable artifact reference and retention semantics for stored reports |
|
||||||
|
| Workspace & Tenant Closure Lifecycle v1 | Governs closure behavior for broader workspace and tenant truth, not only artifacts | Reuses read-only and retained-history rules without collapsing artifact lifecycle into closure state |
|
||||||
|
| Data Export Before Deletion v1 | Owns export-request workflow, export bundle contents, proof of completion, and delete preconditions | Reuses the distinction between direct download of an existing artifact and future pre-deletion export workflow |
|
||||||
|
| Retention & Purge Governance v1 | Owns irreversible deletion, purge scheduling, purge proof, and hard-delete execution | Reuses hold and deletion-request semantics as preconditions rather than re-inventing them |
|
||||||
|
| Enterprise Access Boundary & Support Access Governance v1 | Owns support-access approval, TTL, and customer-visible access audit, not artifact retention truth | Reuses audit-trail principles but stays out of artifact lifecycle modeling |
|
||||||
|
|
||||||
|
## Final Direction
|
||||||
|
|
||||||
|
This spec turns governance artifacts into first-class retained records with one honest runtime contract. The contract is intentionally smaller than a portal, a purge engine, or a workflow framework: it gives every in-scope artifact a stable reference, separates lifecycle role from retention posture, explains what suspended-read-only means for retained history, and requires audit proof for lifecycle-sensitive actions that remain inside bounded current-owner seams. It also keeps the roadmap clean by consuming Spec 262 as closed taxonomy input, by treating Spec 158 as context rather than a new foundation, by keeping accepted-risk decision adoption headless in this slice, and by leaving Stored Reports Surface, export-before-delete, purge governance, closure lifecycle, and support-access governance as explicit follow-up slices.
|
||||||
224
specs/267-artifact-lifecycle-retention/tasks.md
Normal file
224
specs/267-artifact-lifecycle-retention/tasks.md
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
description: "Task list for Governance Artifact Lifecycle & Retention v1"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: Governance Artifact Lifecycle & Retention v1
|
||||||
|
|
||||||
|
**Input**: Design documents from `specs/267-artifact-lifecycle-retention/`
|
||||||
|
**Prerequisites**: `specs/267-artifact-lifecycle-retention/spec.md`, `specs/267-artifact-lifecycle-retention/plan.md`, `specs/267-artifact-lifecycle-retention/research.md`, `specs/267-artifact-lifecycle-retention/data-model.md`, `specs/267-artifact-lifecycle-retention/quickstart.md`, `specs/267-artifact-lifecycle-retention/checklists/requirements.md`
|
||||||
|
|
||||||
|
**Review Artifact**: `specs/267-artifact-lifecycle-retention/checklists/requirements.md` is the outcome-of-record for the review outcome class, workflow outcome, and test-governance outcome. If mutation persistence widens or a current decision-surface edit becomes necessary, update that artifact before continuing.
|
||||||
|
**Tests**: REQUIRED (Pest). Keep proof bounded to one new shared contract unit suite plus focused feature coverage on existing evidence, review-pack, tenant-review, customer-workspace, findings, and stored-report seams. The canonical proving suite is `GovernanceArtifactLifecycleContractTest`, `EvidenceSnapshotResourceTest`, `EvidenceSnapshotAuditLogTest`, `ReviewPackResourceTest`, `ReviewPackDownloadTest`, `ReviewPackEntitlementEnforcementTest`, `TenantReviewLifecycleTest`, `TenantReviewUiContractTest`, `CustomerReviewWorkspacePackAccessTest`, `CustomerReviewWorkspaceLaunchLinksTest`, `FindingExceptionRenewalTest`, `FindingExceptionRevocationTest`, `StoredReportModelTest`, `PruneStoredReportsCommandTest`, and `StoredReportFingerprintTest`. Browser proof is not part of the default lane; one narrow manual smoke is allowed only if native Feature or controller coverage leaves grouped-action or read-only behavior ambiguous.
|
||||||
|
**Operations**: No new `OperationRun`, queue family, export-before-delete workflow, or purge executor is allowed. Existing review-pack generation must keep the shared `OperationRun` start and completion UX unchanged, and blocked future starts remain pre-run business-state blocks.
|
||||||
|
**RBAC**: Workspace entitlement and tenant entitlement remain mandatory before any artifact truth, lifecycle badge, download affordance, or mutation affordance is revealed. Non-members and wrong-scope actors stay `404`; in-scope members missing the relevant capability stay `403`. Existing capabilities such as `EVIDENCE_VIEW`, `REVIEW_PACK_VIEW`, `REVIEW_PACK_MANAGE`, `TENANT_REVIEW_VIEW`, `TENANT_REVIEW_MANAGE`, and the current `FINDING_EXCEPTION_*` checks remain authoritative. Destructive-like lifecycle actions must stay confirmation-backed on existing detail owners only.
|
||||||
|
**Shared Pattern Reuse**: Reuse `ArtifactTruthPresenter`, `BadgeCatalog`, `BadgeRenderer`, `WorkspaceCommercialLifecycleResolver`, existing review-pack download authorization, current tenant-review and evidence lifecycle services, current findings aggregate seams, and existing audit logging. Do not create a generic artifact registry, a second lifecycle presenter family, or a new browsing console.
|
||||||
|
**Filament / Panel Guardrails**: Filament stays v5 on Livewire v4. Provider registration remains unchanged in `apps/platform/bootstrap/providers.php`. `EvidenceSnapshotResource`, `TenantReviewResource`, and `ReviewPackResource` remain non-searchable, no new globally searchable resource is introduced, and no asset-registration or panel-path change is allowed.
|
||||||
|
**Organization**: Tasks are grouped by user story so the shared contract, suspended-read-only behavior, and audit plus decision-history rules remain independently implementable and testable on existing repo seams only.
|
||||||
|
|
||||||
|
## Test Governance Checklist
|
||||||
|
|
||||||
|
- [ ] Lane assignment stays `Unit` plus focused `Feature`, which is the narrowest sufficient proof for this slice.
|
||||||
|
- [ ] New or changed tests stay in `apps/platform/tests/Unit/Support/GovernanceArtifactTruth/`, `apps/platform/tests/Feature/Evidence/`, `apps/platform/tests/Feature/ReviewPack/`, `apps/platform/tests/Feature/TenantReview/`, `apps/platform/tests/Feature/Reviews/`, `apps/platform/tests/Feature/Findings/`, `apps/platform/tests/Feature/PermissionPosture/`, and `apps/platform/tests/Feature/EntraAdminRoles/` only.
|
||||||
|
- [ ] Shared fixtures remain cheap by default through `apps/platform/tests/Feature/Concerns/BuildsGovernanceArtifactTruthFixtures.php` and the current family factories instead of a new heavy artifact matrix harness.
|
||||||
|
- [ ] Planned proof uses the canonical commands from `plan.md` and `quickstart.md` without widening into browser or unrelated suites.
|
||||||
|
- [ ] The declared surface profile remains `standard-native-filament` plus `shared-detail-family`; `CustomerReviewWorkspace` stays read-only and scan-first, and accepted-risk decision adoption stays headless in v1.
|
||||||
|
- [ ] `specs/267-artifact-lifecycle-retention/checklists/requirements.md` records the current outcome-of-record for the shipped slice. If mutation persistence or current decision-surface edits widen scope, the workflow outcome flips to `split` before implementation continues.
|
||||||
|
- [ ] Any drift toward a generic artifact registry, purge engine, closure flow, billing or support-access scope, new browsing console, panel or global-search changes, or browser-heavy default proof resolves as `reject-or-split` or an explicit follow-up spec.
|
||||||
|
|
||||||
|
## Implementation Status
|
||||||
|
|
||||||
|
- [x] Delivered the shared read-only lifecycle and retention contract on the existing evidence, tenant-review, and review-pack detail surfaces plus headless stored-report and accepted-risk decision-history seams.
|
||||||
|
- [x] Preserved the existing suspended-read-only and signed-download behavior on current services and controller seams without widening into a new console or queue family.
|
||||||
|
- [x] Ran the canonical feature and unit proof plus one narrow manual browser smoke in tenant scope.
|
||||||
|
- [x] Split hold and deletion-request persistence out of this slice because the bounded current-owner mutation gate did not pass without widening scope.
|
||||||
|
- [ ] Deferred mutation persistence, new hold or deletion-request actions, and any broader purge or closure workflow to a follow-up slice.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Confirm the bounded slice, current shared seams, and stop conditions before implementation begins.
|
||||||
|
|
||||||
|
- [ ] T001 Review `specs/267-artifact-lifecycle-retention/spec.md`, `specs/267-artifact-lifecycle-retention/plan.md`, `specs/267-artifact-lifecycle-retention/research.md`, `specs/267-artifact-lifecycle-retention/data-model.md`, `specs/267-artifact-lifecycle-retention/quickstart.md`, and `specs/267-artifact-lifecycle-retention/checklists/requirements.md` together so lifecycle truth, retention truth, proof commands, workflow outcomes, and explicit non-goals stay aligned.
|
||||||
|
- [ ] T002 [P] Confirm the current shared lifecycle-truth seams in `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php`, `apps/platform/app/Support/Badges/BadgeCatalog.php`, and `apps/platform/app/Support/Badges/BadgeRenderer.php` before adding any new lifecycle or retention vocabulary.
|
||||||
|
- [ ] T003 [P] Confirm the current artifact owners, review-context surfaces, and lifecycle services in `apps/platform/app/Models/EvidenceSnapshot.php`, `apps/platform/app/Models/TenantReview.php`, `apps/platform/app/Models/ReviewPack.php`, `apps/platform/app/Models/StoredReport.php`, `apps/platform/app/Models/FindingException.php`, `apps/platform/app/Models/FindingExceptionDecision.php`, `apps/platform/app/Services/Evidence/EvidenceSnapshotService.php`, `apps/platform/app/Services/ReviewPackService.php`, `apps/platform/app/Services/TenantReviews/TenantReviewLifecycleService.php`, `apps/platform/app/Services/Findings/FindingExceptionService.php`, and `apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php`.
|
||||||
|
- [ ] T004 [P] Confirm the current consumer surfaces and controller seams in `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`, `apps/platform/app/Filament/Resources/TenantReviewResource.php`, `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`, `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, and `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Establish the shared governance-artifact lifecycle and retention contract before any story-specific surface work begins.
|
||||||
|
|
||||||
|
**Critical**: No user-story implementation should begin until this phase is complete.
|
||||||
|
|
||||||
|
- [x] T005 [P] Add failing unit coverage in `apps/platform/tests/Unit/Support/GovernanceArtifactTruth/GovernanceArtifactLifecycleContractTest.php` for immutable artifact reference, lifecycle versus retention separation, hold-over-deletion-request precedence, suspended-read-only not acting as retention truth, and family mapping for evidence snapshots, review packs, stored reports, and accepted-risk decision history.
|
||||||
|
- [ ] T006 [P] Extend shared test setup in `apps/platform/tests/Feature/Concerns/BuildsGovernanceArtifactTruthFixtures.php`, `apps/platform/database/factories/TenantReviewFactory.php`, `apps/platform/database/factories/ReviewPackFactory.php`, and `apps/platform/database/factories/StoredReportFactory.php` so existing suites can seed current, historical, superseded, retained, expired-direct-access, and suspended-read-only cases without a new heavy-governance harness.
|
||||||
|
- [x] T007 Implement the bounded shared lifecycle and retention contract in `apps/platform/app/Support/Ui/GovernanceArtifactTruth/ArtifactTruthPresenter.php` so the current presenter emits immutable reference, lifecycle state, retention state, and next allowed or blocked action from one path instead of page-local status fragments.
|
||||||
|
- [x] T008 [P] Centralize lifecycle and retention badge vocabulary in `apps/platform/app/Support/Badges/BadgeCatalog.php` and `apps/platform/app/Support/Badges/BadgeRenderer.php` so evidence, review, review-pack, customer-workspace, and aggregate-level decision history reuse one meaning for `current`, `historical`, `superseded`, `retained`, `hold`, `deletion requested`, and `expired direct access`.
|
||||||
|
|
||||||
|
**Checkpoint**: The shared contract, fixtures, and centralized badge language are ready before current surfaces start consuming them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Understand What An Artifact Is Allowed To Do Now (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Give operators one stable artifact reference, one lifecycle role, one retention state, and one honest next action across the existing evidence, review, and review-pack surfaces while keeping stored-report adoption headless.
|
||||||
|
|
||||||
|
**Independent Test**: Open an evidence snapshot, a tenant review with a current export summary, and a review pack in current, historical, and expired-direct-access states, then verify each surface answers identity, lifecycle, retention, and next action from the first screen without opening raw diagnostics.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [x] T009 [P] [US1] Add failing feature coverage in `apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php` for immutable reference display, lifecycle and retention truth, historical-versus-current evidence semantics, and one dominant next action on the existing evidence resource.
|
||||||
|
- [x] T010 [P] [US1] Add failing feature coverage in `apps/platform/tests/Feature/TenantReview/TenantReviewUiContractTest.php` and `apps/platform/tests/Feature/ReviewPack/ReviewPackResourceTest.php` for current export summaries, retained-versus-historical review-pack truth, blocked-versus-allowed next action wording, and the absence of page-local lifecycle drift.
|
||||||
|
- [ ] T011 [P] [US1] Add failing headless coverage in `apps/platform/tests/Feature/PermissionPosture/StoredReportModelTest.php` and `apps/platform/tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php` for stable stored-report reference, latest-versus-historical fingerprint selection, and retention-truth derivation without introducing a dedicated stored-report browsing surface.
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [x] T012 [US1] Adopt the shared lifecycle contract on `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php` and `apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php` so evidence summary surfaces show artifact reference, lifecycle state, retention state, and the next allowed or blocked action without new local badge maps.
|
||||||
|
- [x] T013 [US1] Adopt the shared lifecycle contract on `apps/platform/app/Filament/Resources/TenantReviewResource.php` and `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php` so the current export summary states whether the referenced artifact is current, historical, retained, or blocked without widening the review workflow.
|
||||||
|
- [x] T014 [US1] Adopt the shared lifecycle contract on `apps/platform/app/Filament/Resources/ReviewPackResource.php` and `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php` so retained and downloadable packs, historical packs, and expired-direct-access packs all speak one lifecycle and retention language.
|
||||||
|
- [ ] T015 [US1] Extend headless stored-report adoption in `apps/platform/app/Models/StoredReport.php` and `apps/platform/app/Console/Commands/PruneStoredReportsCommand.php` so the shared contract can classify current-versus-historical stored reports and retention expectations without creating a stored-report UI or generic purge framework.
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 1 is independently functional when the current artifact surfaces and headless stored-report seams all consume the same lifecycle and retention contract.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Preserve History During Suspended Read-Only Posture (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Preserve authorized read access to retained artifacts and already-generated downloads during suspended-read-only posture while keeping generation and lifecycle mutations blocked on the current surfaces.
|
||||||
|
|
||||||
|
**Independent Test**: Put a workspace into suspended-read-only posture, then verify that retained review packs and review-linked artifacts remain readable on current surfaces while generation or mutation affordances stay blocked with one calm read-only explanation.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [ ] T016 [P] [US2] Add failing feature coverage in `apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php`, `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php`, and `apps/platform/tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php` for retained-pack access, blocked future generation, read-only explanation fidelity, and keeping `CustomerReviewWorkspace` scan-first instead of turning it into a new download console.
|
||||||
|
- [ ] T017 [P] [US2] Add failing feature coverage in `apps/platform/tests/Feature/TenantReview/TenantReviewLifecycleTest.php` for review-derived artifact truth under suspended-read-only posture and for separating blocked successor generation from still-readable retained history.
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [ ] T018 [US2] Extend `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` and `apps/platform/app/Services/ReviewPackService.php` so ready retained downloads keep the current entitled-access behavior while blocked new generation continues to reuse `apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php` without becoming retention truth.
|
||||||
|
- [ ] T019 [US2] Adopt suspended-read-only lifecycle messaging in `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php`, `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, and `apps/platform/app/Filament/Resources/ReviewPackResource.php` so retained history stays visible and tenant-plane mutations stay honestly blocked.
|
||||||
|
- [ ] T020 [US2] Keep the existing action-surface contract intact in `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` and `apps/platform/app/Filament/Resources/ReviewPackResource.php` so `Open review` and `Download current pack` remain contextual actions, `CustomerReviewWorkspace` stays read-only, and no new browsing console or mutation surface appears.
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 2 is independently functional when suspended-read-only posture preserves retained history without implying new generation, hidden deletion, or a broader console rewrite.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Audit Every Lifecycle-Sensitive Artifact Action (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Keep lifecycle-sensitive actions auditable and authorized while extending the same lifecycle contract to accepted-risk decision history through current aggregate seams only.
|
||||||
|
|
||||||
|
**Independent Test**: Download a retained review pack, perform any in-scope lifecycle mutation that passes the bounded current-owner persistence gate, and inspect accepted-risk decision history through the current aggregate seam to confirm audit proof, authorization behavior, append-only history, and no new decision or artifact console.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [ ] T021 [P] [US3] Add failing feature coverage in `apps/platform/tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php` and `apps/platform/tests/Feature/ReviewPack/ReviewPackDownloadTest.php` for artifact-reference-based audit events that record actor, workspace, tenant, source surface, and before-or-after lifecycle or retention truth on download and any lifecycle-sensitive action that passes the bounded current-owner persistence gate.
|
||||||
|
- [ ] T022 [P] [US3] Add failing feature coverage in `apps/platform/tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php`, `apps/platform/tests/Feature/Evidence/EvidenceSnapshotResourceTest.php`, and `apps/platform/tests/Feature/TenantReview/TenantReviewLifecycleTest.php` for positive and negative authorization, `404` versus `403` behavior, and destructive-like action gating on current detail owners only.
|
||||||
|
- [ ] T023 [P] [US3] Add failing feature coverage in `apps/platform/tests/Feature/Findings/FindingExceptionRenewalTest.php` and `apps/platform/tests/Feature/Findings/FindingExceptionRevocationTest.php` for headless accepted-risk decision-history lifecycle adoption on the current aggregate seam, append-only historical addressability, and the absence of current-slice `DecisionRegister` or `ViewFindingException` changes.
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [ ] T024 [US3] Extend `apps/platform/app/Services/Evidence/EvidenceSnapshotService.php`, `apps/platform/app/Services/ReviewPackService.php`, and `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` so lifecycle-sensitive actions reuse stable audit action IDs and artifact-reference-based audit payloads instead of family-local prose drift, while mutation support remains conditional on the bounded current-owner persistence gate.
|
||||||
|
- [ ] T025 [US3] Reuse existing capability and policy enforcement in `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `apps/platform/app/Filament/Resources/EvidenceSnapshotResource/Pages/ViewEvidenceSnapshot.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php`, `apps/platform/app/Filament/Resources/TenantReviewResource/Pages/ViewTenantReview.php`, and `apps/platform/app/Http/Controllers/ReviewPackDownloadController.php` so detail visibility, linked artifact summaries, direct download, and any bounded mutation paths that remain on evidence or review-pack owners keep correct `404` versus `403` semantics.
|
||||||
|
- [ ] T026 [US3] Adopt the shared contract on `apps/platform/app/Models/FindingException.php`, `apps/platform/app/Models/FindingExceptionDecision.php`, and `apps/platform/app/Services/Findings/FindingExceptionService.php` so accepted-risk decision history stays append-only, historically addressable, and headless in this slice.
|
||||||
|
- [x] T027 [US3] Before adding any new hold, release-hold, or deletion-request support, confirm it can stay on the current evidence-snapshot or review-pack owners and matching detail surfaces only; if not, split the mutation slice and ship read-only lifecycle truth plus existing download audit without widening into current decision surfaces, a cross-family artifact table, purge executor, closure flow, or support-access scope.
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 3 is independently functional when lifecycle-sensitive actions are auditable, authorization stays honest, and decision history adopts the contract headlessly without widening past current seams.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Validation
|
||||||
|
|
||||||
|
**Purpose**: Run the canonical proving suite, format touched files, and explicitly verify that the slice stayed inside its bounded non-goals.
|
||||||
|
|
||||||
|
- [x] T028 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/GovernanceArtifactTruth/GovernanceArtifactLifecycleContractTest.php`.
|
||||||
|
- [x] T029 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Evidence/EvidenceSnapshotResourceTest.php tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php`.
|
||||||
|
- [x] T030 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/ReviewPackResourceTest.php tests/Feature/ReviewPack/ReviewPackDownloadTest.php tests/Feature/ReviewPack/ReviewPackEntitlementEnforcementTest.php`.
|
||||||
|
- [x] T031 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/TenantReview/TenantReviewLifecycleTest.php tests/Feature/TenantReview/TenantReviewUiContractTest.php tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php`.
|
||||||
|
- [x] T032 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingExceptionRevocationTest.php`.
|
||||||
|
- [x] T033 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/StoredReportModelTest.php tests/Feature/PermissionPosture/PruneStoredReportsCommandTest.php tests/Feature/EntraAdminRoles/StoredReportFingerprintTest.php`.
|
||||||
|
- [x] T034 [P] Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` for the touched platform files.
|
||||||
|
- [x] T035 [P] Review the implementation against `specs/267-artifact-lifecycle-retention/spec.md`, `specs/267-artifact-lifecycle-retention/plan.md`, and `specs/267-artifact-lifecycle-retention/checklists/requirements.md` to confirm there is no generic artifact registry, no purge engine, no closure flow, no billing or support-access scope, no new browsing console, no current-slice `DecisionRegister` or `ViewFindingException` change, no new global-search behavior, no panel-provider change outside `apps/platform/bootstrap/providers.php`, and no new asset-registration path.
|
||||||
|
- [ ] T036 [P] Only if native Feature and controller proof still leaves grouped-action or read-only behavior ambiguous, perform one narrow manual smoke on `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` using the scenarios from `specs/267-artifact-lifecycle-retention/quickstart.md`.
|
||||||
|
- [x] T036 [P] Only if native Feature and controller proof still leaves grouped-action or read-only behavior ambiguous, perform one narrow manual smoke on `apps/platform/app/Filament/Resources/EvidenceSnapshotResource.php`, `apps/platform/app/Filament/Resources/ReviewPackResource.php`, and `apps/platform/app/Filament/Pages/Reviews/CustomerReviewWorkspace.php` using the scenarios from `specs/267-artifact-lifecycle-retention/quickstart.md`.
|
||||||
|
- [x] T037 [P] Re-check `specs/267-artifact-lifecycle-retention/checklists/requirements.md` and keep the recorded outcomes at `acceptable-special-case` / `split` / `keep`; if the bounded current-owner persistence gate fails or a current decision-surface edit becomes necessary, update the workflow outcome to `split` before merge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||||
|
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user-story work.
|
||||||
|
- **Phase 3 (US1)**: depends on Phase 2 and establishes the visible contract on current artifact surfaces plus headless stored-report adoption.
|
||||||
|
- **Phase 4 (US2)**: depends on Phase 3 because suspended-read-only behavior must consume the shared lifecycle truth already visible on review and pack surfaces.
|
||||||
|
- **Phase 5 (US3)**: depends on Phase 2 and should land after US1 so audit and decision-history adoption tighten the same shared contract instead of creating a parallel path.
|
||||||
|
- **Phase 6 (Polish)**: depends on all desired user stories being complete.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: independently testable after Phase 2 and delivers the core lifecycle and retention truth.
|
||||||
|
- **US2 (P1)**: independently testable after US1 and delivers the suspended-read-only trust contract without new console work.
|
||||||
|
- **US3 (P2)**: independently testable after Phase 2 and hardens audit, authorization, and headless decision-history adoption on existing seams.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Land the listed Pest coverage first and make it fail for the intended gap.
|
||||||
|
- Reuse `ArtifactTruthPresenter`, shared badges, existing services, and existing detail owners before considering any new support type.
|
||||||
|
- Re-run the narrowest relevant canonical proof command before moving to the next story checkpoint.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Work Examples
|
||||||
|
|
||||||
|
### US1 Parallel Example
|
||||||
|
|
||||||
|
- T009, T010, and T011 can run in parallel because they touch different test families.
|
||||||
|
- After T007 and T008 land, T012, T013, T014, and T015 can split between evidence, tenant-review consumer surfaces, review pack, and stored-report headless adoption.
|
||||||
|
|
||||||
|
### US2 Parallel Example
|
||||||
|
|
||||||
|
- T016 and T017 can run in parallel because they cover different read-only seams.
|
||||||
|
- After US1 is stable, T018 and T019 can run in parallel if one contributor owns controller/service behavior and another owns Filament page messaging; T020 should merge after both to keep the surface contract coherent.
|
||||||
|
|
||||||
|
### US3 Parallel Example
|
||||||
|
|
||||||
|
- T021, T022, and T023 can run in parallel because they cover audit, authorization, and decision-history suites separately.
|
||||||
|
- T024 and T026 can proceed in parallel after the shared contract is stable; T025 and T027 should merge after them so policy and mutation boundaries reflect the final behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Suggested MVP Scope
|
||||||
|
|
||||||
|
- MVP = **US1** if the team needs the smallest shippable increment for truthful lifecycle and retention language.
|
||||||
|
- Release-ready scope should include **US2** before merge whenever suspended-read-only behavior is part of the target acceptance gate for the environment.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Complete Phase 1 and Phase 2.
|
||||||
|
2. Deliver US1 on current artifact surfaces plus headless stored-report adoption.
|
||||||
|
3. Add US2 to preserve suspended-read-only retained-history access and block mutation truthfully.
|
||||||
|
4. Add US3 for audit, authorization, and decision-history adoption on existing seams.
|
||||||
|
5. Finish with the canonical proof commands and bounded smoke only if native proof remains ambiguous.
|
||||||
|
|
||||||
|
### Team Strategy
|
||||||
|
|
||||||
|
1. Settle the shared contract and badge vocabulary first.
|
||||||
|
2. Parallelize test work by family before runtime edits widen.
|
||||||
|
3. Serialize merges around `ArtifactTruthPresenter`, `ReviewPackDownloadController`, `CustomerReviewWorkspace`, and the accepted-risk aggregate seams so cross-surface lifecycle language stays consistent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deferred Follow-Ups / Non-Goals
|
||||||
|
|
||||||
|
- retention and purge governance, including irreversible deletion executors and schedulers
|
||||||
|
- workspace or tenant closure lifecycle work
|
||||||
|
- billing, subscription, or commercial-state truth changes beyond current suspended-read-only reuse
|
||||||
|
- support-access governance, delegated access, or impersonation scope
|
||||||
|
- any generic artifact registry, workflow engine, or broad customer artifact portal
|
||||||
|
- a dedicated stored-report browsing surface or any current-slice rewrite of existing Spec 265 decision surfaces
|
||||||
|
- export-before-deletion workflow and bundle proof
|
||||||
|
- provider-lifecycle expansion beyond current artifact truth
|
||||||
|
- panel, global-search, provider-registration, or asset-strategy changes for this slice
|
||||||
Loading…
Reference in New Issue
Block a user