feat: polish decision register evidence operation run links #362
@ -310,13 +310,16 @@ public function table(Table $table): Table
|
||||
->sortable(),
|
||||
TextColumn::make('proof_availability')
|
||||
->label('Proof')
|
||||
->state(function (FindingException $record): string {
|
||||
$referenceCount = (int) data_get($record->evidence_summary ?? [], 'reference_count', 0);
|
||||
|
||||
return $referenceCount > 0
|
||||
? $referenceCount.' evidence linked'
|
||||
: 'No linked proof';
|
||||
})
|
||||
->state(fn (FindingException $record): string => (string) ($this->rowPayload($record)['proof_label'] ?? 'No linked proof'))
|
||||
->description(fn (FindingException $record): ?string => $this->proofUrl($record) !== null
|
||||
? (string) ($this->rowPayload($record)['proof_url_label'] ?? 'View proof')
|
||||
: null)
|
||||
->url(fn (FindingException $record): ?string => $this->proofUrl($record))
|
||||
->wrap(),
|
||||
TextColumn::make('operation_run_link')
|
||||
->label('Operation')
|
||||
->state(fn (FindingException $record): string => (string) ($this->rowPayload($record)['operation_run_label'] ?? 'No operation linked'))
|
||||
->url(fn (FindingException $record): ?string => $this->operationRunUrl($record))
|
||||
->wrap(),
|
||||
TextColumn::make('next_action_label')
|
||||
->label('Next action')
|
||||
@ -700,6 +703,27 @@ public function decisionUrl(FindingException $record): ?string
|
||||
);
|
||||
}
|
||||
|
||||
private function proofUrl(FindingException $record): ?string
|
||||
{
|
||||
$row = $this->rowPayload($record);
|
||||
$proofState = $row['proof_state'] ?? null;
|
||||
|
||||
if ($proofState === 'linked_detail_section') {
|
||||
return $this->decisionUrl($record);
|
||||
}
|
||||
|
||||
$url = $row['proof_url'] ?? null;
|
||||
|
||||
return is_string($url) && $url !== '' ? $url : null;
|
||||
}
|
||||
|
||||
private function operationRunUrl(FindingException $record): ?string
|
||||
{
|
||||
$url = $this->rowPayload($record)['operation_run_url'] ?? null;
|
||||
|
||||
return is_string($url) && $url !== '' ? $url : null;
|
||||
}
|
||||
|
||||
private function navigationContext(): CanonicalNavigationContext
|
||||
{
|
||||
return CanonicalNavigationContext::forDecisionRegister(
|
||||
|
||||
@ -4,12 +4,24 @@
|
||||
|
||||
namespace App\Support\GovernanceDecisions;
|
||||
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Filament\Resources\FindingExceptionResource;
|
||||
use App\Filament\Resources\StoredReportResource;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\FindingExceptionEvidenceReference;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\StoredReport;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\OperationRunLinks;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
final readonly class GovernanceDecisionRegisterBuilder
|
||||
{
|
||||
@ -51,7 +63,7 @@ public function build(Workspace $workspace, array $visibleTenants, string $regis
|
||||
$rows = FindingException::query()
|
||||
->where('workspace_id', (int) $workspace->getKey())
|
||||
->whereIn('managed_environment_id', $visibleTenantIds)
|
||||
->with(['tenant:id,name', 'owner:id,name', 'currentDecision'])
|
||||
->with(['tenant', 'owner:id,name', 'currentDecision', 'evidenceReferences', 'finding.currentRun', 'finding.baselineRun'])
|
||||
->get()
|
||||
->map(fn (FindingException $exception): ?array => $this->buildRow($exception))
|
||||
->filter()
|
||||
@ -101,6 +113,9 @@ private function buildRow(FindingException $exception): ?array
|
||||
return null;
|
||||
}
|
||||
|
||||
$proofMetadata = $this->buildProofMetadata($exception);
|
||||
$operationRunMetadata = $this->buildOperationRunMetadata($exception);
|
||||
|
||||
return [
|
||||
'exception_id' => (int) $exception->getKey(),
|
||||
'register_state' => $registerState,
|
||||
@ -116,9 +131,305 @@ private function buildRow(FindingException $exception): ?array
|
||||
: null,
|
||||
'due_at' => $exception->review_due_at ?? $exception->expires_at,
|
||||
'decision_at' => $currentDecision->decided_at,
|
||||
...$proofMetadata,
|
||||
...$operationRunMetadata,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* proof_count: int,
|
||||
* proof_state: string,
|
||||
* proof_label: string,
|
||||
* proof_url: string|null,
|
||||
* proof_url_label: string|null,
|
||||
* proof_unavailable_reason: string|null,
|
||||
* }
|
||||
*/
|
||||
private function buildProofMetadata(FindingException $exception): array
|
||||
{
|
||||
$references = $this->evidenceReferences($exception);
|
||||
$proofCount = $references->count();
|
||||
|
||||
if ($proofCount === 0) {
|
||||
return [
|
||||
'proof_count' => 0,
|
||||
'proof_state' => 'not_linked',
|
||||
'proof_label' => 'No linked proof',
|
||||
'proof_url' => null,
|
||||
'proof_url_label' => null,
|
||||
'proof_unavailable_reason' => null,
|
||||
];
|
||||
}
|
||||
|
||||
if ($proofCount === 1) {
|
||||
$directLink = $this->resolveDirectProofLink($exception, $references->first());
|
||||
|
||||
if (is_array($directLink)) {
|
||||
return [
|
||||
'proof_count' => $proofCount,
|
||||
'proof_state' => $directLink['state'],
|
||||
'proof_label' => $this->proofLabel($proofCount),
|
||||
'proof_url' => $directLink['url'],
|
||||
'proof_url_label' => $directLink['label'],
|
||||
'proof_unavailable_reason' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'proof_count' => $proofCount,
|
||||
'proof_state' => 'linked_detail_section',
|
||||
'proof_label' => $this->proofLabel($proofCount),
|
||||
'proof_url' => $this->findingExceptionDetailUrl($exception),
|
||||
'proof_url_label' => 'View proof',
|
||||
'proof_unavailable_reason' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* operation_run_state: string,
|
||||
* operation_run_url: string|null,
|
||||
* operation_run_label: string,
|
||||
* }
|
||||
*/
|
||||
private function buildOperationRunMetadata(FindingException $exception): array
|
||||
{
|
||||
$run = $this->sourceFindingOperationRun($exception);
|
||||
|
||||
if (! $run instanceof OperationRun) {
|
||||
$runs = $this->evidenceOperationRunsFor($exception)
|
||||
->unique(fn (OperationRun $run): int => (int) $run->getKey())
|
||||
->values();
|
||||
|
||||
if ($runs->count() !== 1) {
|
||||
return [
|
||||
'operation_run_state' => $runs->isEmpty() ? 'not_linked' : 'run_not_available',
|
||||
'operation_run_url' => null,
|
||||
'operation_run_label' => 'No operation linked',
|
||||
];
|
||||
}
|
||||
|
||||
/** @var OperationRun $run */
|
||||
$run = $runs->first();
|
||||
}
|
||||
|
||||
if (! $this->canViewOperationRun($run, $exception)) {
|
||||
return [
|
||||
'operation_run_state' => 'run_not_available',
|
||||
'operation_run_url' => null,
|
||||
'operation_run_label' => 'No operation linked',
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'operation_run_state' => 'linked_run',
|
||||
'operation_run_url' => OperationRunLinks::tenantlessView($run),
|
||||
'operation_run_label' => 'View operation',
|
||||
];
|
||||
}
|
||||
|
||||
private function sourceFindingOperationRun(FindingException $exception): ?OperationRun
|
||||
{
|
||||
$finding = $exception->relationLoaded('finding')
|
||||
? $exception->finding
|
||||
: $exception->finding()->with(['currentRun', 'baselineRun'])->first();
|
||||
|
||||
if (! $finding instanceof Finding) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($finding->currentRun instanceof OperationRun) {
|
||||
return $finding->currentRun;
|
||||
}
|
||||
|
||||
return $finding->baselineRun instanceof OperationRun
|
||||
? $finding->baselineRun
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, FindingExceptionEvidenceReference>
|
||||
*/
|
||||
private function evidenceReferences(FindingException $exception): Collection
|
||||
{
|
||||
if ($exception->relationLoaded('evidenceReferences')) {
|
||||
return $exception->evidenceReferences
|
||||
->filter(fn (mixed $reference): bool => $reference instanceof FindingExceptionEvidenceReference)
|
||||
->values();
|
||||
}
|
||||
|
||||
return $exception->evidenceReferences()->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{state: string, url: string, label: string}|null
|
||||
*/
|
||||
private function resolveDirectProofLink(FindingException $exception, ?FindingExceptionEvidenceReference $reference): ?array
|
||||
{
|
||||
if (! $reference instanceof FindingExceptionEvidenceReference) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isEvidenceSnapshotReference($reference)) {
|
||||
$snapshot = $this->resolveEvidenceSnapshot($exception, $reference);
|
||||
|
||||
if ($snapshot instanceof EvidenceSnapshot && $this->canViewEvidenceSnapshot($snapshot)) {
|
||||
return [
|
||||
'state' => 'linked_evidence',
|
||||
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], panel: 'admin', tenant: $snapshot->tenant),
|
||||
'label' => 'View evidence',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->isStoredReportReference($reference)) {
|
||||
$report = $this->resolveStoredReport($exception, $reference);
|
||||
|
||||
if ($report instanceof StoredReport && $this->canViewStoredReport($report)) {
|
||||
return [
|
||||
'state' => 'linked_report',
|
||||
'url' => StoredReportResource::getUrl('view', ['record' => $report], panel: 'admin', tenant: $report->tenant),
|
||||
'label' => 'View report',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, OperationRun>
|
||||
*/
|
||||
private function evidenceOperationRunsFor(FindingException $exception): Collection
|
||||
{
|
||||
return $this->evidenceReferences($exception)
|
||||
->filter(fn (FindingExceptionEvidenceReference $reference): bool => $this->isEvidenceSnapshotReference($reference))
|
||||
->map(fn (FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot => $this->resolveEvidenceSnapshot($exception, $reference))
|
||||
->filter(fn (?EvidenceSnapshot $snapshot): bool => $snapshot instanceof EvidenceSnapshot)
|
||||
->map(fn (EvidenceSnapshot $snapshot): ?OperationRun => $snapshot->operationRun)
|
||||
->filter(fn (?OperationRun $run): bool => $run instanceof OperationRun)
|
||||
->values();
|
||||
}
|
||||
|
||||
private function resolveEvidenceSnapshot(FindingException $exception, FindingExceptionEvidenceReference $reference): ?EvidenceSnapshot
|
||||
{
|
||||
$sourceId = $this->numericSourceId($reference);
|
||||
|
||||
if ($sourceId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return EvidenceSnapshot::query()
|
||||
->with(['tenant', 'operationRun'])
|
||||
->whereKey($sourceId)
|
||||
->where('workspace_id', (int) $exception->workspace_id)
|
||||
->where('managed_environment_id', (int) $exception->managed_environment_id)
|
||||
->first();
|
||||
}
|
||||
|
||||
private function resolveStoredReport(FindingException $exception, FindingExceptionEvidenceReference $reference): ?StoredReport
|
||||
{
|
||||
$sourceId = $this->numericSourceId($reference);
|
||||
|
||||
if ($sourceId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StoredReport::query()
|
||||
->with('tenant')
|
||||
->whereKey($sourceId)
|
||||
->where('workspace_id', (int) $exception->workspace_id)
|
||||
->where('managed_environment_id', (int) $exception->managed_environment_id)
|
||||
->whereIn('report_type', StoredReportResource::supportedReportTypes())
|
||||
->first();
|
||||
}
|
||||
|
||||
private function canViewEvidenceSnapshot(EvidenceSnapshot $snapshot): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenant = $snapshot->tenant;
|
||||
|
||||
return $user instanceof User
|
||||
&& $tenant instanceof ManagedEnvironment
|
||||
&& $user->canAccessTenant($tenant)
|
||||
&& $user->can(Capabilities::EVIDENCE_VIEW, $tenant);
|
||||
}
|
||||
|
||||
private function canViewStoredReport(StoredReport $report): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
$tenant = $report->tenant;
|
||||
$capability = StoredReportResource::capabilityForReportType((string) $report->report_type);
|
||||
|
||||
return $user instanceof User
|
||||
&& $tenant instanceof ManagedEnvironment
|
||||
&& is_string($capability)
|
||||
&& $user->canAccessTenant($tenant)
|
||||
&& $user->can($capability, $tenant);
|
||||
}
|
||||
|
||||
private function canViewOperationRun(OperationRun $run, FindingException $exception): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
return $user instanceof User
|
||||
&& $this->runMatchesExceptionScope($run, $exception)
|
||||
&& Gate::forUser($user)->allows('view', $run);
|
||||
}
|
||||
|
||||
private function runMatchesExceptionScope(OperationRun $run, FindingException $exception): bool
|
||||
{
|
||||
return (int) $run->workspace_id === (int) $exception->workspace_id
|
||||
&& (int) $run->managed_environment_id === (int) $exception->managed_environment_id;
|
||||
}
|
||||
|
||||
private function findingExceptionDetailUrl(FindingException $exception): ?string
|
||||
{
|
||||
$tenant = $exception->tenant;
|
||||
|
||||
if (! $tenant instanceof ManagedEnvironment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return FindingExceptionResource::getUrl('view', ['record' => $exception], panel: 'admin', tenant: $tenant);
|
||||
}
|
||||
|
||||
private function proofLabel(int $proofCount): string
|
||||
{
|
||||
return $proofCount === 1
|
||||
? '1 proof item'
|
||||
: $proofCount.' proof items';
|
||||
}
|
||||
|
||||
private function numericSourceId(FindingExceptionEvidenceReference $reference): ?int
|
||||
{
|
||||
if (! is_string($reference->source_id) && ! is_numeric($reference->source_id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourceId = trim((string) $reference->source_id);
|
||||
|
||||
if ($sourceId === '' || ! ctype_digit($sourceId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sourceId = (int) $sourceId;
|
||||
|
||||
return $sourceId > 0 ? $sourceId : null;
|
||||
}
|
||||
|
||||
private function isEvidenceSnapshotReference(FindingExceptionEvidenceReference $reference): bool
|
||||
{
|
||||
return in_array((string) $reference->source_type, ['evidence_snapshot', EvidenceSnapshot::class], true);
|
||||
}
|
||||
|
||||
private function isStoredReportReference(FindingExceptionEvidenceReference $reference): bool
|
||||
{
|
||||
return in_array((string) $reference->source_type, ['stored_report', StoredReport::class], true);
|
||||
}
|
||||
|
||||
private function resolveRegisterState(FindingException $exception, FindingExceptionDecision $currentDecision): ?string
|
||||
{
|
||||
$status = (string) $exception->status;
|
||||
@ -153,4 +464,4 @@ private function isRecentlyClosed(?CarbonInterface $decidedAt): bool
|
||||
|
||||
return $decidedAt->greaterThanOrEqualTo(now()->startOfDay()->subDays(self::RECENTLY_CLOSED_DAYS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,11 +3,17 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\User;
|
||||
use App\Services\Findings\FindingExceptionService;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
pest()->browser()->timeout(20_000);
|
||||
@ -64,8 +70,36 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
|
||||
ensureDefaultMicrosoftProviderConnection: false,
|
||||
);
|
||||
|
||||
$exceptionWithProof = spec265ApprovedFindingException($tenant, $user);
|
||||
spec265ApprovedFindingException($tenant, $user);
|
||||
|
||||
$run = OperationRun::factory()->forTenant($tenant)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$snapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$exceptionWithProof->forceFill(['evidence_summary' => ['reference_count' => 1]])->save();
|
||||
$exceptionWithProof->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $snapshot->getKey(),
|
||||
'label' => 'Current evidence snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$decisionRegisterUrl = DecisionRegister::getUrl(panel: 'admin', parameters: [
|
||||
'managed_environment_id' => (string) $tenant->getKey(),
|
||||
]);
|
||||
@ -81,7 +115,26 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
|
||||
->assertNoConsoleLogs()
|
||||
->assertSee('The register is currently filtered to one environment.')
|
||||
->assertSee($tenant->name)
|
||||
->assertSee('Showing 1 result')
|
||||
->assertSee('Visible rows: 2')
|
||||
->assertSee('1 proof item')
|
||||
->assertSee('View evidence')
|
||||
->assertSee('View operation')
|
||||
->assertSee('No linked proof')
|
||||
->assertSee('No operation linked')
|
||||
->click('View evidence')
|
||||
->waitForText('Snapshot')
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
visit($decisionRegisterUrl)
|
||||
->waitForText('Decision register')
|
||||
->click('View operation')
|
||||
->waitForText('Operation #'.((int) $run->getKey()))
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
visit($decisionRegisterUrl)
|
||||
->waitForText('Decision register')
|
||||
->assertSeeIn('tbody tr.fi-ta-row:first-of-type', $tenant->name)
|
||||
->click('tbody tr.fi-ta-row:first-of-type')
|
||||
->waitForText('Opened from the workspace decision register')
|
||||
@ -96,5 +149,5 @@ function spec265SmokeLoginUrl(User $user, ManagedEnvironment $tenant, string $re
|
||||
->assertNoConsoleLogs()
|
||||
->assertSee('The register is currently filtered to one environment.')
|
||||
->assertSee($tenant->name)
|
||||
->assertSee('Showing 1 result');
|
||||
->assertSee('Visible rows: 2');
|
||||
});
|
||||
|
||||
@ -3,10 +3,14 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
|
||||
it('keeps the decision register read-only with one dominant row action', function (): void {
|
||||
@ -112,4 +116,153 @@
|
||||
->assertOk()
|
||||
->assertSee('Recent closure reason')
|
||||
->assertDontSee('Old closure reason');
|
||||
});
|
||||
});
|
||||
|
||||
it('does not leak cross-workspace evidence artifact links from the decision register', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'name' => 'Visible ManagedEnvironment',
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$otherTenant = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'name' => 'Other workspace ManagedEnvironment',
|
||||
]);
|
||||
|
||||
$otherSnapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $otherTenant->workspace_id,
|
||||
'managed_environment_id' => (int) $otherTenant->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$finding = Finding::factory()->for($tenant)->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
]);
|
||||
|
||||
$exception = FindingException::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'owner_user_id' => (int) $user->getKey(),
|
||||
'status' => FindingException::STATUS_PENDING,
|
||||
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
'request_reason' => 'Cross workspace proof link test',
|
||||
'requested_at' => now()->subDay(),
|
||||
'review_due_at' => now()->addDay(),
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
]);
|
||||
|
||||
$decision = $exception->decisions()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'actor_user_id' => (int) $user->getKey(),
|
||||
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
||||
'reason' => 'Cross workspace proof link test',
|
||||
'metadata' => [],
|
||||
'decided_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
||||
$exception->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $otherSnapshot->getKey(),
|
||||
'label' => 'Other workspace snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(DecisionRegister::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertSee('1 proof item')
|
||||
->assertSee('View proof')
|
||||
->assertDontSee('View evidence')
|
||||
->assertDontSee('/admin/t', false);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $otherSnapshot], tenant: $otherTenant, panel: 'admin'))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('does not leak cross-environment evidence artifact links from the decision register', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'name' => 'Visible ManagedEnvironment',
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$otherTenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'status' => 'active',
|
||||
'name' => 'Other environment ManagedEnvironment',
|
||||
]);
|
||||
|
||||
$otherSnapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $otherTenant->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$finding = Finding::factory()->for($tenant)->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
]);
|
||||
|
||||
$exception = FindingException::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
'requested_by_user_id' => (int) $user->getKey(),
|
||||
'owner_user_id' => (int) $user->getKey(),
|
||||
'status' => FindingException::STATUS_PENDING,
|
||||
'current_validity_state' => FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
'request_reason' => 'Cross environment proof link test',
|
||||
'requested_at' => now()->subDay(),
|
||||
'review_due_at' => now()->addDay(),
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
]);
|
||||
|
||||
$decision = $exception->decisions()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'actor_user_id' => (int) $user->getKey(),
|
||||
'decision_type' => FindingExceptionDecision::TYPE_REQUESTED,
|
||||
'reason' => 'Cross environment proof link test',
|
||||
'metadata' => [],
|
||||
'decided_at' => now()->subDay(),
|
||||
]);
|
||||
|
||||
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
||||
$exception->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $otherSnapshot->getKey(),
|
||||
'label' => 'Other environment snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(DecisionRegister::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertSee('1 proof item')
|
||||
->assertSee('View proof')
|
||||
->assertDontSee('View evidence')
|
||||
->assertDontSee('/admin/t', false);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(EvidenceSnapshotResource::getUrl('view', ['record' => $otherSnapshot], tenant: $otherTenant, panel: 'admin'))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
@ -10,9 +11,13 @@
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Models\WorkspaceMembership;
|
||||
use App\Support\Auth\Capabilities;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Filament\Facades\Filament;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
@ -169,6 +174,50 @@
|
||||
->assertRedirect(DecisionRegister::getUrl(panel: 'admin', parameters: ['register_state' => 'recently_closed']));
|
||||
});
|
||||
|
||||
it('does not render direct evidence links when the actor lacks evidence destination access', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create(['status' => 'active']);
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
Gate::define(Capabilities::EVIDENCE_VIEW, fn (): bool => false);
|
||||
|
||||
$snapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$exception = decisionRegisterAuthException(
|
||||
tenant: $tenant,
|
||||
actor: $user,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'Evidence access denied request',
|
||||
);
|
||||
|
||||
$exception->forceFill(['evidence_summary' => ['reference_count' => 1]])->save();
|
||||
$exception->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $snapshot->getKey(),
|
||||
'label' => 'Evidence snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(DecisionRegister::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertSee('1 proof item')
|
||||
->assertSee('View proof')
|
||||
->assertDontSee('View evidence')
|
||||
->assertDontSee('/admin/t', false);
|
||||
});
|
||||
|
||||
function decisionRegisterAuthException(
|
||||
ManagedEnvironment $tenant,
|
||||
User $actor,
|
||||
|
||||
@ -3,11 +3,17 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Governance\DecisionRegister;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\Finding;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\User;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
@ -130,6 +136,81 @@
|
||||
->assertSee('No recently closed decisions match this filter right now.');
|
||||
});
|
||||
|
||||
it('renders proof and operation affordances only when real links are available', function (): void {
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'status' => 'active',
|
||||
'name' => 'Proof ManagedEnvironment',
|
||||
'external_id' => 'proof-tenant',
|
||||
]);
|
||||
[$user, $tenant] = createUserWithTenant($tenant, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->forTenant($tenant)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$snapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$withProof = decisionRegisterPageException(
|
||||
tenant: $tenant,
|
||||
actor: $user,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'Proof-backed request',
|
||||
exceptionAttributes: [
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
'review_due_at' => now()->addDay(),
|
||||
],
|
||||
);
|
||||
|
||||
$withProof->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $tenant->workspace_id,
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $snapshot->getKey(),
|
||||
'label' => 'Current evidence snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
decisionRegisterPageException(
|
||||
tenant: $tenant,
|
||||
actor: $user,
|
||||
status: FindingException::STATUS_ACTIVE,
|
||||
validityState: FindingException::VALIDITY_VALID,
|
||||
decisionType: FindingExceptionDecision::TYPE_APPROVED,
|
||||
decisionReason: 'No proof request',
|
||||
exceptionAttributes: [
|
||||
'approved_by_user_id' => (int) $user->getKey(),
|
||||
'approved_at' => now()->subDay(),
|
||||
'effective_from' => now()->subDay(),
|
||||
'evidence_summary' => ['reference_count' => 0],
|
||||
'review_due_at' => now()->addDays(2),
|
||||
],
|
||||
);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(DecisionRegister::getUrl(panel: 'admin'))
|
||||
->assertOk()
|
||||
->assertSee('1 proof item')
|
||||
->assertSee('View evidence')
|
||||
->assertSee('View operation')
|
||||
->assertSee('No linked proof')
|
||||
->assertSee('No operation linked')
|
||||
->assertDontSee('/admin/t', false);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $exceptionAttributes
|
||||
* @param array<string, mixed> $decisionAttributes
|
||||
|
||||
@ -3,12 +3,20 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Finding;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\FindingException;
|
||||
use App\Models\FindingExceptionDecision;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\StoredReport;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\GovernanceDecisions\GovernanceDecisionRegisterBuilder;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
@ -200,6 +208,372 @@
|
||||
->and($payload['rows'][0]['next_action_label'])->toBe('Review follow-up');
|
||||
});
|
||||
|
||||
it('exposes missing proof and aggregate detail proof states truthfully', function (): void {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
|
||||
$withoutProof = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'No proof linked',
|
||||
);
|
||||
|
||||
$withMultipleProof = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_ACTIVE,
|
||||
validityState: FindingException::VALIDITY_VALID,
|
||||
decisionType: FindingExceptionDecision::TYPE_APPROVED,
|
||||
decisionReason: 'Multiple proof linked',
|
||||
exceptionAttributes: [
|
||||
'approved_by_user_id' => (int) $actor->getKey(),
|
||||
'approved_at' => now()->subDays(3),
|
||||
'effective_from' => now()->subDays(3),
|
||||
'evidence_summary' => ['reference_count' => 2],
|
||||
'review_due_at' => now()->addDays(2),
|
||||
],
|
||||
);
|
||||
|
||||
$withMultipleProof->evidenceReferences()->createMany([
|
||||
[
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => 'snapshot-001',
|
||||
'label' => 'Snapshot summary',
|
||||
'summary_payload' => [],
|
||||
],
|
||||
[
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'review_pack',
|
||||
'source_id' => 'review-pack-001',
|
||||
'label' => 'Review pack summary',
|
||||
'summary_payload' => [],
|
||||
],
|
||||
]);
|
||||
|
||||
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
||||
workspace: $workspace,
|
||||
visibleTenants: [$tenant],
|
||||
registerState: 'open',
|
||||
);
|
||||
|
||||
$rows = collect($payload['rows'])->keyBy('exception_id');
|
||||
|
||||
expect($rows[(int) $withoutProof->getKey()])
|
||||
->toMatchArray([
|
||||
'proof_count' => 0,
|
||||
'proof_state' => 'not_linked',
|
||||
'proof_label' => 'No linked proof',
|
||||
'proof_url' => null,
|
||||
'proof_url_label' => null,
|
||||
'operation_run_state' => 'not_linked',
|
||||
'operation_run_url' => null,
|
||||
'operation_run_label' => 'No operation linked',
|
||||
])
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_count'])->toBe(2)
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_state'])->toBe('linked_detail_section')
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_label'])->toBe('2 proof items')
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_url'])->toContain('/admin/workspaces/')
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_url'])->not->toContain('/admin/t')
|
||||
->and($rows[(int) $withMultipleProof->getKey()]['proof_url_label'])->toBe('View proof');
|
||||
});
|
||||
|
||||
it('links a single same-scope evidence snapshot and its operation when authorized', function (): void {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
|
||||
$this->actingAs($actor);
|
||||
|
||||
$run = OperationRun::factory()->forTenant($tenant)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$snapshot = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$exception = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_ACTIVE,
|
||||
validityState: FindingException::VALIDITY_VALID,
|
||||
decisionType: FindingExceptionDecision::TYPE_APPROVED,
|
||||
decisionReason: 'Evidence snapshot proof',
|
||||
exceptionAttributes: [
|
||||
'approved_by_user_id' => (int) $actor->getKey(),
|
||||
'approved_at' => now()->subDay(),
|
||||
'effective_from' => now()->subDay(),
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
],
|
||||
);
|
||||
|
||||
$exception->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $snapshot->getKey(),
|
||||
'label' => 'Evidence snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
||||
workspace: $workspace,
|
||||
visibleTenants: [$tenant],
|
||||
registerState: 'open',
|
||||
);
|
||||
|
||||
$row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());
|
||||
|
||||
expect($row)->toMatchArray([
|
||||
'proof_count' => 1,
|
||||
'proof_state' => 'linked_evidence',
|
||||
'proof_label' => '1 proof item',
|
||||
'proof_url_label' => 'View evidence',
|
||||
'operation_run_state' => 'linked_run',
|
||||
'operation_run_label' => 'View operation',
|
||||
])
|
||||
->and($row['proof_url'])->toContain('/admin/workspaces/')
|
||||
->and($row['proof_url'])->toContain('/evidence/')
|
||||
->and($row['proof_url'])->not->toContain('/admin/t')
|
||||
->and($row['operation_run_url'])->toBe(OperationRunLinks::tenantlessView($run))
|
||||
->and($row['operation_run_url'])->not->toContain('/admin/t');
|
||||
});
|
||||
|
||||
it('links a same-scope source finding operation when no evidence operation exists', function (): void {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
|
||||
$this->actingAs($actor);
|
||||
|
||||
$run = OperationRun::factory()->forTenant($tenant)->create([
|
||||
'type' => 'tenant.finding.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$finding = Finding::factory()->for($tenant)->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'current_operation_run_id' => (int) $run->getKey(),
|
||||
]);
|
||||
|
||||
$exception = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'Finding source operation',
|
||||
exceptionAttributes: [
|
||||
'finding_id' => (int) $finding->getKey(),
|
||||
],
|
||||
);
|
||||
|
||||
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
||||
workspace: $workspace,
|
||||
visibleTenants: [$tenant],
|
||||
registerState: 'open',
|
||||
);
|
||||
|
||||
$row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());
|
||||
|
||||
expect($row)->toMatchArray([
|
||||
'proof_count' => 0,
|
||||
'proof_state' => 'not_linked',
|
||||
'proof_label' => 'No linked proof',
|
||||
'operation_run_state' => 'linked_run',
|
||||
'operation_run_label' => 'View operation',
|
||||
'operation_run_url' => OperationRunLinks::tenantlessView($run),
|
||||
])
|
||||
->and($row['operation_run_url'])->not->toContain('/admin/t');
|
||||
});
|
||||
|
||||
it('links a single same-scope stored report when authorized', function (): void {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
|
||||
$this->actingAs($actor);
|
||||
|
||||
$report = StoredReport::factory()->permissionPosture()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'fingerprint' => hash('sha256', 'decision-register-report'),
|
||||
]);
|
||||
|
||||
$exception = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'Stored report proof',
|
||||
exceptionAttributes: [
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
],
|
||||
);
|
||||
|
||||
$exception->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'stored_report',
|
||||
'source_id' => (string) $report->getKey(),
|
||||
'label' => 'Permission posture report',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
||||
workspace: $workspace,
|
||||
visibleTenants: [$tenant],
|
||||
registerState: 'open',
|
||||
);
|
||||
|
||||
$row = collect($payload['rows'])->firstWhere('exception_id', (int) $exception->getKey());
|
||||
|
||||
expect($row)->toMatchArray([
|
||||
'proof_count' => 1,
|
||||
'proof_state' => 'linked_report',
|
||||
'proof_label' => '1 proof item',
|
||||
'proof_url_label' => 'View report',
|
||||
'operation_run_state' => 'not_linked',
|
||||
'operation_run_url' => null,
|
||||
])
|
||||
->and($row['proof_url'])->toContain('/stored-reports/')
|
||||
->and($row['proof_url'])->not->toContain('/admin/t');
|
||||
});
|
||||
|
||||
it('does not invent direct artifact or operation links from loose identifiers or cross-scope runs', function (): void {
|
||||
$workspace = Workspace::factory()->create();
|
||||
$actor = User::factory()->create();
|
||||
$tenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
createUserWithTenant(tenant: $tenant, user: $actor, role: 'owner', workspaceRole: 'owner');
|
||||
$otherTenant = ManagedEnvironment::factory()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
]);
|
||||
createUserWithTenant(tenant: $otherTenant, user: $actor, role: 'owner', workspaceRole: 'owner');
|
||||
$this->actingAs($actor);
|
||||
|
||||
$otherRun = OperationRun::factory()->forTenant($otherTenant)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
]);
|
||||
|
||||
$sameScopeSnapshotWithOtherRun = EvidenceSnapshot::query()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'operation_run_id' => (int) $otherRun->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => ['finding_count' => 1],
|
||||
'generated_at' => now(),
|
||||
]);
|
||||
|
||||
$looseIdentifier = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_PENDING,
|
||||
validityState: FindingException::VALIDITY_MISSING_SUPPORT,
|
||||
decisionType: FindingExceptionDecision::TYPE_REQUESTED,
|
||||
decisionReason: 'Loose proof identifier',
|
||||
exceptionAttributes: [
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
],
|
||||
);
|
||||
|
||||
$looseIdentifier->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => 'snapshot-001',
|
||||
'label' => 'Loose evidence snapshot',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$crossScopeRun = makeFindingExceptionWithCurrentDecision(
|
||||
tenant: $tenant,
|
||||
owner: $actor,
|
||||
actor: $actor,
|
||||
status: FindingException::STATUS_ACTIVE,
|
||||
validityState: FindingException::VALIDITY_VALID,
|
||||
decisionType: FindingExceptionDecision::TYPE_APPROVED,
|
||||
decisionReason: 'Cross-scope operation should not link',
|
||||
exceptionAttributes: [
|
||||
'approved_by_user_id' => (int) $actor->getKey(),
|
||||
'approved_at' => now()->subDay(),
|
||||
'effective_from' => now()->subDay(),
|
||||
'evidence_summary' => ['reference_count' => 1],
|
||||
],
|
||||
);
|
||||
|
||||
$crossScopeRun->evidenceReferences()->create([
|
||||
'workspace_id' => (int) $workspace->getKey(),
|
||||
'managed_environment_id' => (int) $tenant->getKey(),
|
||||
'source_type' => 'evidence_snapshot',
|
||||
'source_id' => (string) $sameScopeSnapshotWithOtherRun->getKey(),
|
||||
'label' => 'Snapshot with cross-scope run',
|
||||
'summary_payload' => [],
|
||||
]);
|
||||
|
||||
$payload = app(GovernanceDecisionRegisterBuilder::class)->build(
|
||||
workspace: $workspace,
|
||||
visibleTenants: [$tenant],
|
||||
registerState: 'open',
|
||||
);
|
||||
|
||||
$rows = collect($payload['rows'])->keyBy('exception_id');
|
||||
|
||||
expect($rows[(int) $looseIdentifier->getKey()])
|
||||
->toMatchArray([
|
||||
'proof_count' => 1,
|
||||
'proof_state' => 'linked_detail_section',
|
||||
'proof_url_label' => 'View proof',
|
||||
'operation_run_state' => 'not_linked',
|
||||
'operation_run_url' => null,
|
||||
])
|
||||
->and($rows[(int) $crossScopeRun->getKey()])
|
||||
->toMatchArray([
|
||||
'proof_state' => 'linked_evidence',
|
||||
'proof_url_label' => 'View evidence',
|
||||
'operation_run_state' => 'run_not_available',
|
||||
'operation_run_url' => null,
|
||||
'operation_run_label' => 'No operation linked',
|
||||
]);
|
||||
});
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $exceptionAttributes
|
||||
* @param array<string, mixed> $decisionAttributes
|
||||
@ -248,4 +622,4 @@ function makeFindingExceptionWithCurrentDecision(
|
||||
$exception->forceFill(['current_decision_id' => (int) $decision->getKey()])->save();
|
||||
|
||||
return $exception->fresh(['tenant', 'owner', 'currentDecision']);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ # TenantPilot Implementation Ledger
|
||||
> **Last reviewed:** 2026-05-12
|
||||
> **Use for:** Repo-based implementation status and product-surface maturity assessment
|
||||
> **Do not use for:** Roadmap priority, spec priority, or proof that tests were executed in the current branch
|
||||
> **Scoped maintenance:** 2026-05-15 Decision Register reconciliation update after Spec 306; 2026-05-15 Tenant Panel dead-code retirement guardrail update after Spec 304; 2026-05-12 roadmap/ledger alignment after the admin workspace navigation and tenant-owned surface repair candidate intake from the repo-verified navigation/panel audit; 2026-05-06 ledger conflict cleanup plus alignment with `docs/product/roadmap.md` and `docs/product/spec-candidates.md` after the cross-domain indicator candidate intake and the current manual-promotion backlog review.
|
||||
> **Scoped maintenance:** 2026-05-15 Decision Register proof-link implementation update after Spec 307; 2026-05-15 Decision Register reconciliation update after Spec 306; 2026-05-15 Tenant Panel dead-code retirement guardrail update after Spec 304; 2026-05-12 roadmap/ledger alignment after the admin workspace navigation and tenant-owned surface repair candidate intake from the repo-verified navigation/panel audit; 2026-05-06 ledger conflict cleanup plus alignment with `docs/product/roadmap.md` and `docs/product/spec-candidates.md` after the cross-domain indicator candidate intake and the current manual-promotion backlog review.
|
||||
|
||||
## Purpose
|
||||
|
||||
@ -23,7 +23,7 @@ ## Purpose
|
||||
|
||||
## Current Product Position
|
||||
|
||||
TenantPilot ist aktuell ein starkes internes Governance- und Operations-Produkt mit belastbaren Foundations fuer Execution Truth, Baselines/Drift, Findings, Evidence, Reviews, Review Packs, Supportability, Telemetry, Safety Controls und eine repo-reale governed AI policy foundation. Darauf sitzen inzwischen mehrere repo-real productization slices: eine customer-safe Review-/Governance-Package-Surface im Admin-Kontext, released-review detail handoff, compliance interpretation overlays, bounded external support-desk handoff, commercial lifecycle state handling mit read-only gating, eine kanonische cross-tenant compare preview mit promotion preflight sowie ein repo-verifizierter operatorseitiger Decision Register ueber bestehender FindingException-Decision-Truth. Die Repo-Wahrheit liegt damit klar ueber einer simplen Lesart von "R1 done / R2 partial" und auch ueber einer rein foundation-only Interpretation fuer Reviews, Support und Portfolio-Preparation. Gleichzeitig ist das Produkt noch nicht voll als kundenseitig konsumierbare Portfolio- und Commercial-Plattform ausgereift: Es fehlen die letzte customer-safe self-serve productization ueber der Review-Surface, actual portfolio promotion execution, ein schmaler Decision-Register-Proof-Link-Follow-up, wiederholbare Billing-/Subscription-Truth, eine klarere Stored-Reports-Surface und der erste governed AI runtime consumer ueber der bereits repo-realen AI policy foundation.
|
||||
TenantPilot ist aktuell ein starkes internes Governance- und Operations-Produkt mit belastbaren Foundations fuer Execution Truth, Baselines/Drift, Findings, Evidence, Reviews, Review Packs, Supportability, Telemetry, Safety Controls und eine repo-reale governed AI policy foundation. Darauf sitzen inzwischen mehrere repo-real productization slices: eine customer-safe Review-/Governance-Package-Surface im Admin-Kontext, released-review detail handoff, compliance interpretation overlays, bounded external support-desk handoff, commercial lifecycle state handling mit read-only gating, eine kanonische cross-tenant compare preview mit promotion preflight sowie ein repo-verifizierter operatorseitiger Decision Register ueber bestehender FindingException-Decision-Truth mit direkter proof/run link polish. Die Repo-Wahrheit liegt damit klar ueber einer simplen Lesart von "R1 done / R2 partial" und auch ueber einer rein foundation-only Interpretation fuer Reviews, Support und Portfolio-Preparation. Gleichzeitig ist das Produkt noch nicht voll als kundenseitig konsumierbare Portfolio- und Commercial-Plattform ausgereift: Es fehlen die letzte customer-safe self-serve productization ueber der Review-Surface, actual portfolio promotion execution, Decision-Register-Review-Pack-/customer-safe follow-through, wiederholbare Billing-/Subscription-Truth, eine klarere Stored-Reports-Surface und der erste governed AI runtime consumer ueber der bereits repo-realen AI policy foundation.
|
||||
|
||||
## Runtime Guardrails
|
||||
|
||||
@ -81,7 +81,7 @@ ## Implemented Capabilities
|
||||
| Drift findings and governance pressure | sellable | yes | yes | repo tests, not run | yes | yes | `app/Models/Finding.php`; `app/Filament/Widgets/Dashboard/RecentDriftFindings.php`; `tests/Feature/Findings/*` |
|
||||
| Findings inboxes and governance inbox | fast sellable | yes | yes | repo tests, not run | yes | yes | `app/Filament/Pages/Findings/MyFindingsInbox.php`; `app/Filament/Pages/Findings/FindingsIntakeQueue.php`; `app/Filament/Pages/Governance/GovernanceInbox.php`; `tests/Feature/Findings/MyWorkInboxTest.php`; `tests/Feature/Governance/*` |
|
||||
| Finding exceptions and risk acceptance workflow | fast sellable | yes | yes | repo tests, not run | yes | yes | `app/Models/FindingException.php`; `app/Services/Findings/FindingExceptionService.php`; `app/Filament/Resources/FindingExceptionResource.php`; `tests/Feature/Findings/FindingExceptionWorkflowTest.php` |
|
||||
| Decision Register operator surface | implemented but not productized | yes | yes | repo tests, run in Spec 306 | yes | no | `specs/265-decision-register-approval/spec.md`; `app/Filament/Pages/Governance/DecisionRegister.php`; `app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`; `tests/Feature/Governance/DecisionRegisterPageTest.php`; `tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php` |
|
||||
| Decision Register operator surface | implemented but not productized | yes | yes | repo tests, run in Specs 306/307 | yes | no | `specs/265-decision-register-approval/spec.md`; `specs/307-decision-register-evidence-operationrun-link-polish/spec.md`; `app/Filament/Pages/Governance/DecisionRegister.php`; `app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`; `tests/Feature/Governance/DecisionRegisterPageTest.php`; `tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php`; `tests/Feature/Findings/FindingExceptionDecisionRegisterBoundariesTest.php` |
|
||||
| Restore workflow with safety gates | sellable | yes | yes | repo tests, not run | yes | yes | `app/Models/OperationRun.php`; restore gates and tests in `tests/Feature/Restore/*` |
|
||||
| Evidence snapshots | foundation-only | yes | yes | repo tests, not run | yes | no | `app/Models/EvidenceSnapshot.php`; `app/Services/Evidence/EvidenceSnapshotService.php`; `tests/Feature/Evidence/*` |
|
||||
| Tenant reviews | fast sellable | yes | yes | repo tests, not run | yes | yes | `app/Models/TenantReview.php`; `app/Services/TenantReviews/TenantReviewService.php`; `tests/Feature/TenantReview/*` |
|
||||
@ -130,7 +130,7 @@ ## Fast-Sellable Or Not-Yet-Productized Capabilities
|
||||
|
||||
- Customer-facing review consumption: Tenant Reviews, Evidence Snapshots, Review Packs, the Customer Review Workspace, the customer-safe released-review detail mode, governance-package delivery cues, compliance interpretation overlays, and commercial-lifecycle-aware access states are repo-real; broader lifecycle/governance taxonomy work remains separate.
|
||||
- Findings Workflow v2: Triage, Assignment, My Work, Intake, Governance Inbox, Exceptions, notifications, and the three queue-facing cleanup/hardening follow-through packages are now repo-backed; later cross-tenant action layers remain separate work.
|
||||
- Decision Register: Spec 265 operator register runtime and tests are repo-backed; direct evidence/report and OperationRun proof-link polish remains a narrow productization gap, not a Greenfield v1.
|
||||
- Decision Register: Spec 265 operator register runtime and Spec 307 direct evidence/report plus source/evidence OperationRun proof-link polish are repo-backed; customer-safe summary and review-pack inclusion remain separate follow-ups, not a Greenfield v1.
|
||||
- Product scalability and self-service: Onboarding, Support, Help, Entitlements, commercial lifecycle state handling, and external support-desk handoff are repo-real; broader trial/demo and billing-subscription truth still remain.
|
||||
- MSP portfolio operations: Portfolio-Triage plus cross-tenant compare preview and promotion preflight are repo-real; actual promotion execution and broader portfolio action orchestration remain open.
|
||||
- Platform operations maturity: Control Tower und Ops Controls sind stark, aber einige geplante operatorseitige Drilldowns/Exports fehlen noch.
|
||||
@ -227,7 +227,7 @@ ## Open Gaps & Blockers
|
||||
| Workspace-first / ManagedEnvironment core cutover is not started | Strategic architecture blocker | The repo still centers many governance, support, and portfolio surfaces on `Tenant` semantics, so future multi-provider work would otherwise accumulate more tenant- and Microsoft-centric core coupling | Platform core / provider-neutral posture | `Workspace-first / ManagedEnvironment Core Cutover` pack in `docs/product/spec-candidates.md` (planned as Specs 279-287) |
|
||||
| Auditor-ready executive export is still missing | Productization blocker | Review truth remains short of auditor-/executive-ready delivery, even though the dedicated follow-through is now spec-backed | R2 review delivery | `specs/263-auditor-pack-executive-export/spec.md` |
|
||||
| Cross-tenant promotion execution is still missing | Product blocker | Compare preview and preflight are repo-real, but the actual portfolio action remains absent even though the execution package is now spec-backed | MSP Portfolio & Operations | `specs/264-cross-tenant-promotion-execution/spec.md` |
|
||||
| Decision Register proof-link polish is still pending | Productization gap | The operator register exists, but direct evidence/report, `OperationRun`, and review-pack proof links are not fully productized on the register path | Decision-based operating | `decision-register-evidence-operationrun-link-polish` |
|
||||
| Decision Register customer-safe/review-pack inclusion is still missing | Productization gap | The operator register now exposes scoped proof/run links, but customer-safe consumption and review-pack inclusion remain separate product choices | Decision-based operating | `decision-register-review-pack-inclusion` or `decision-register-customer-safe-summary` |
|
||||
| Governance-artifact lifecycle runtime is still missing | Trust / auditability blocker | Lifecycle taxonomy and point retention rules exist, but governance artifacts still lack immutable-reference, hold, export, delete, and suspended/read-only runtime semantics | Lifecycle governance / enterprise trust | `Governance Artifact Lifecycle & Retention v1` |
|
||||
| Cross-domain progress and indicator semantics guardrail is still missing | UX / trust guardrail | Bars, percentages, scores, readiness, risk, usage, and generation-state hints still lack one shared taxonomy and standards layer above the OperationRun-specific rules | UI semantics / product trust | `Cross-Domain Progress / Indicator Semantics candidate group` |
|
||||
| Admin workspace navigation and tenant-owned surface contract drift remains open | UX / IA repair blocker | Inventory and adjacent tenant-owned admin surfaces still conflict between workspace-home clean-sidebar rules and valid environment-bound admin access, leaving active product breaks and stale hide-first assumptions in place | UI maturity / admin runtime contract | `admin-inventory-navigation-cutover` from the `Admin Workspace Navigation & Tenant-owned Surface Repair candidate group` |
|
||||
@ -243,7 +243,7 @@ ## Recommended Manual Promotions
|
||||
- `Cross-Domain Progress / Indicator Semantics candidate group` -> anchored by `specs/268-operationrun-activity-feedback/spec.md`, `specs/270-operationrun-progress-contract/spec.md`, `specs/271-counted-progress-rollout/spec.md`, `specs/272-operationrun-phase-composite-progress/spec.md`, `docs/ui/tenantpilot-enterprise-ui-standards.md`, and the current progress-like UI seams called out in `docs/product/spec-candidates.md`
|
||||
- `Admin Workspace Navigation & Tenant-owned Surface Repair candidate group` -> anchored by `apps/platform/app/Filament/Clusters/Inventory/InventoryCluster.php`, `apps/platform/app/Filament/Pages/InventoryCoverage.php`, `apps/platform/app/Filament/Resources/InventoryItemResource.php`, `apps/platform/app/Filament/Resources/EntraGroupResource.php`, `apps/platform/app/Filament/Concerns/WorkspaceScopedTenantRoutes.php`, `apps/platform/app/Support/OperateHub/OperateHubShell.php`, and the navigation/runtime tests called out in `docs/product/spec-candidates.md`; promote `admin-inventory-navigation-cutover` first, then keep the route audit, groups cutover, contract split, and tenant-panel dead-code retirement as separate sequenced follow-through
|
||||
- `Workspace-first / ManagedEnvironment Core Cutover` pack -> anchored by `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, `docs/product/implementation-ledger.md`, and the tenant-centric platform seams already visible across review, support, portfolio, and governance surfaces; keep it as a clean development-stage cutover pack rather than a compatibility-layer program
|
||||
- `decision-register-evidence-operationrun-link-polish` -> anchored by `specs/265-decision-register-approval/spec.md`, `specs/306-decision-register-reconciliation/decision-register-reconciliation.md`, `apps/platform/app/Filament/Pages/Governance/DecisionRegister.php`, `apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`, and the focused Decision Register tests
|
||||
- `decision-register-review-pack-inclusion` / `decision-register-customer-safe-summary` -> anchored by `specs/265-decision-register-approval/spec.md`, `specs/306-decision-register-reconciliation/decision-register-reconciliation.md`, `specs/307-decision-register-evidence-operationrun-link-polish/spec.md`, `apps/platform/app/Filament/Pages/Governance/DecisionRegister.php`, `apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`, and the focused Decision Register tests
|
||||
- `Governance Artifact Lifecycle & Retention v1` -> anchored by `specs/158-artifact-truth-semantics/spec.md`, `specs/262-lifecycle-governance-taxonomy/spec.md`, and `docs/product/standards/lifecycle-governance.md`
|
||||
- `Billing & Subscription Truth Layer v1` -> anchored by `specs/247-plans-entitlements-billing-readiness/spec.md` and `specs/251-commercial-entitlements-billing-state/spec.md`
|
||||
- `Customer-Facing Localization Adoption v1` -> anchored by `specs/252-platform-localization-v1/spec.md`, `specs/258-customer-review-productization/spec.md`, and `specs/260-governance-service-packaging/spec.md`
|
||||
|
||||
@ -4,7 +4,7 @@ # Spec Candidates
|
||||
> **Last reviewed:** 2026-05-12
|
||||
> **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
|
||||
> **Scoped maintenance:** 2026-05-15 Spec 306 Decision Register reconciliation update; 2026-05-15 Spec 304 Tenant Panel dead-code retirement guardrail update; 2026-05-12 admin workspace navigation and tenant-owned surface repair candidate intake after the repo-verified navigation/panel audit; 2026-05-06 cross-domain progress and indicator semantics candidate intake; 2026-05-04 OperationRun progress maturity plus Tenant Dashboard active-operations summary candidate intake; 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.
|
||||
> **Scoped maintenance:** 2026-05-15 Spec 307 Decision Register proof-link implementation update; 2026-05-15 Spec 306 Decision Register reconciliation update; 2026-05-15 Spec 304 Tenant Panel dead-code retirement guardrail update; 2026-05-12 admin workspace navigation and tenant-owned surface repair candidate intake after the repo-verified navigation/panel audit; 2026-05-06 cross-domain progress and indicator semantics candidate intake; 2026-05-04 OperationRun progress maturity plus Tenant Dashboard active-operations summary candidate intake; 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.
|
||||
> This file is not a wishlist. It tracks only open gaps that are still worth turning into new or refreshed specs.
|
||||
@ -50,9 +50,9 @@ ### Decision-Based Governance Inbox + Decision Register Follow-up
|
||||
|
||||
- **Status markers**: repo-verified, productization gap, roadmap recommendation
|
||||
- **Roadmap lane**: Now
|
||||
- **Current repo truth**: governance inbox, findings queues, operations attention, review follow-up, and Specs 250/257 already anchor the decision surface. Spec 265 plus current runtime/tests now also prove the bounded operator Decision Register is not Greenfield.
|
||||
- **Problem**: The remaining gap is narrower than the former v1 candidate: direct evidence/report and `OperationRun` proof-link polish, plus later customer-safe or review-pack inclusion if productized.
|
||||
- **Deep-Research-derived sharpening**: treat the next slice as a follow-up over Spec 265, not a new Decision Register or approval-engine rebuild.
|
||||
- **Current repo truth**: governance inbox, findings queues, operations attention, review follow-up, and Specs 250/257 already anchor the decision surface. Spec 265 plus Spec 307 runtime/tests now also prove the bounded operator Decision Register and direct proof/run link polish are not Greenfield.
|
||||
- **Problem**: The remaining gap is narrower than the former v1 candidate: later customer-safe consumption or review-pack inclusion if productized.
|
||||
- **Deep-Research-derived sharpening**: keep any next slice as a follow-up over Specs 265/307, not a new Decision Register or approval-engine rebuild.
|
||||
- **Non-goals**: no generic Kanban board, no PSA clone, no XDR incident console, no new decision table, no duplicate approval engine.
|
||||
|
||||
### Governance Artifact Lifecycle & Retention v1
|
||||
@ -983,12 +983,13 @@ #### `tenant-panel-dead-code-retirement`
|
||||
### Decision Register Evidence / OperationRun Link Polish
|
||||
|
||||
- **Priority**: 1
|
||||
- **Status**: reconciled with Spec 265; narrow productization follow-up pending; moved out of Greenfield scope.
|
||||
- **Repo truth**: Spec 265, `DecisionRegister`, `GovernanceDecisionRegisterBuilder`, FindingException detail handoff, and focused tests are repo-verified. The broad Decision Register & Approval Workflow v1 candidate is no longer an open Greenfield candidate.
|
||||
- **Why promotable now**: Spec 306 classifies the current register as partial productization and identifies the narrowest next step as proof-link polish, not a rebuild.
|
||||
- **Status**: promoted to Spec 307 and implemented as a narrow productization follow-up on 2026-05-15; keep this entry as historical sequencing context unless proof/run link polish regresses.
|
||||
- **Repo truth**: Spec 265, Spec 307, `DecisionRegister`, `GovernanceDecisionRegisterBuilder`, FindingException detail handoff, evidence/report proof links, source/evidence `OperationRun` links, and focused tests are repo-verified. The broad Decision Register & Approval Workflow v1 candidate is no longer an open Greenfield candidate.
|
||||
- **Why promoted**: Spec 306 classified the current register as partial productization and identified the narrowest next step as proof-link polish, not a rebuild.
|
||||
- **Why manual promotion only**: this must stay a deliberate product choice because the follow-up should only expose existing evidence/report and `OperationRun` truth where it already exists, preserve existing approval/closure ownership, and avoid a new workflow engine.
|
||||
- **Anchors**:
|
||||
- `specs/265-decision-register-approval/spec.md`
|
||||
- `specs/307-decision-register-evidence-operationrun-link-polish/spec.md`
|
||||
- `specs/306-decision-register-reconciliation/decision-register-reconciliation.md`
|
||||
- `specs/250-decision-governance-inbox/spec.md`
|
||||
- `specs/257-governance-decision-convergence/spec.md`
|
||||
@ -1159,7 +1160,7 @@ ## Promoted to Spec
|
||||
- Workspace, Tenant & Managed Object Lifecycle Governance v1 -> Spec 262 (`lifecycle-governance-taxonomy`)
|
||||
- Auditor Pack Delivery & Executive Export v1 -> Spec 263 (`auditor-pack-executive-export`)
|
||||
- Cross-Tenant Promotion Execution v1 -> Spec 264 (`cross-tenant-promotion-execution`)
|
||||
- Decision Register & Approval Workflow v1 -> Spec 265 (`decision-register-approval`), reconciled by Spec 306; broad Greenfield scope closed, narrow proof-link follow-up remains manual
|
||||
- Decision Register & Approval Workflow v1 -> Spec 265 (`decision-register-approval`), reconciled by Spec 306 and proof-link-polished by Spec 307; broad Greenfield scope closed, later customer-safe/review-pack inclusion remains separate
|
||||
- Queued Execution Reauthorization and Scope Continuity -> Spec 149 (`queued-execution-reauthorization`)
|
||||
- Livewire Context Locking and Trusted-State Reduction -> Spec 152 (`livewire-context-locking`)
|
||||
- Evidence Domain Foundation -> Spec 153 (`evidence-domain-foundation`)
|
||||
|
||||
@ -0,0 +1,60 @@
|
||||
# Requirements Checklist: Decision Register Evidence / OperationRun Link Polish
|
||||
|
||||
**Purpose**: Validate Spec 307 preparation quality before runtime implementation.
|
||||
**Created**: 2026-05-15
|
||||
**Feature**: `/specs/307-decision-register-evidence-operationrun-link-polish/spec.md`
|
||||
|
||||
## Candidate and Scope
|
||||
|
||||
- [x] Candidate is selected from current product roadmap/spec-candidates and matches the user-provided full draft.
|
||||
- [x] Completed Spec 265 and Spec 306 are treated as context only and are not modified.
|
||||
- [x] Scope is narrow productization polish over the existing Decision Register, not a greenfield rebuild.
|
||||
- [x] Explicit non-goals block new decision persistence, workflow engine, lifecycle actions, evidence storage, Review Pack redesign, customer portal/export, notifications, provider changes, and broad navigation redesign.
|
||||
- [x] The proportionality review explains why derived link metadata is sufficient and why new persistence/abstractions are rejected.
|
||||
|
||||
## Requirements Quality
|
||||
|
||||
- [x] Functional requirements are testable and avoid implementation-only wording where user behavior is the point.
|
||||
- [x] Evidence/report links are allowed only for repo-real references and existing canonical destinations.
|
||||
- [x] OperationRun links are allowed only for repo-real run references and existing link helpers.
|
||||
- [x] Missing proof/run states are truthful and non-alarming.
|
||||
- [x] No fake links may be inferred from labels, counts, statuses, loose text, or timestamps.
|
||||
- [x] No source-of-truth duplication is allowed for evidence payloads, stored report payloads, or OperationRun lifecycle truth.
|
||||
- [x] Existing row-to-FindingException detail handoff is preserved.
|
||||
- [x] Lifecycle action ownership remains on existing queue/detail surfaces.
|
||||
|
||||
## Security, Scope, and Authorization
|
||||
|
||||
- [x] Workspace isolation is explicitly required and tested.
|
||||
- [x] Environment isolation is explicitly required and tested.
|
||||
- [x] Destination pages keep server-side authorization; UI visibility is not treated as authorization.
|
||||
- [x] Non-member/out-of-scope behavior remains deny-as-not-found where existing policy semantics require it.
|
||||
- [x] Member-without-capability destination behavior remains governed by existing policies.
|
||||
- [x] Legacy `/admin/t` URL generation is explicitly forbidden and tested.
|
||||
- [x] Canonical workspace/environment/admin routes are required.
|
||||
|
||||
## Filament and Laravel Compliance
|
||||
|
||||
- [x] Filament v5 and Livewire v4.0+ compliance is stated.
|
||||
- [x] Provider registration location remains `apps/platform/bootstrap/providers.php`; no provider changes are planned.
|
||||
- [x] Relevant globally searchable resources are disabled for global search, so the Edit/View-page hard rule is not newly triggered.
|
||||
- [x] No destructive Decision Register actions are introduced; existing destructive-like detail actions keep confirmation and authorization.
|
||||
- [x] Asset strategy is asset-neutral unless implementation proves otherwise; `filament:assets` deploy impact is called out if assets are added.
|
||||
- [x] Native Filament UI patterns are required and ad-hoc styling is forbidden.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
- [x] Builder unit tests cover proof count, missing proof, direct links where real, multiple-reference fallback, and missing OperationRun.
|
||||
- [x] Feature tests cover page rendering, missing copy, no legacy URLs, detail handoff, lifecycle action absence, and authorization.
|
||||
- [x] Isolation tests cover cross-workspace and cross-environment proof/run link denial.
|
||||
- [x] Artifact and OperationRun link guard lanes are included.
|
||||
- [x] FindingException lifecycle/audit lane is included to prove ownership is unchanged.
|
||||
- [x] `git diff --check` is included.
|
||||
- [x] Browser smoke is recommended and has concrete steps.
|
||||
|
||||
## Readiness Result
|
||||
|
||||
- [x] No unresolved clarification markers are required for preparation.
|
||||
- [x] Requirements, plan, and tasks are mutually aligned.
|
||||
- [x] Runtime implementation can start from `tasks.md` after explicit approval.
|
||||
- [x] Preparation stops before application implementation.
|
||||
@ -0,0 +1,294 @@
|
||||
# Implementation Plan: Decision Register Evidence / OperationRun Link Polish
|
||||
|
||||
**Branch**: `307-decision-register-evidence-operationrun-link-polish` | **Date**: 2026-05-15 | **Spec**: `/specs/307-decision-register-evidence-operationrun-link-polish/spec.md`
|
||||
**Input**: Feature specification from `/specs/307-decision-register-evidence-operationrun-link-polish/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Polish the existing Decision Register proof trail by adding truthful, scoped, read-only evidence/report and OperationRun link metadata to the existing register row projection and native Filament table. The implementation must reuse current `FindingException` / `FindingExceptionDecision` decision truth, current artifact truth (`EvidenceSnapshot`, `StoredReport`), current execution truth (`OperationRun`), and existing canonical route/link helpers. It must not add decision persistence, lifecycle actions, evidence payload storage, OperationRun lifecycle behavior, provider integration, or broad navigation changes.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15
|
||||
**Primary Dependencies**: Laravel 12.52, Filament 5.2.1, Livewire 4.1.4, Laravel Sail, Pest 4.3.1
|
||||
**Storage**: PostgreSQL; no new table or migration expected
|
||||
**Testing**: Pest feature/unit tests through Sail-first commands
|
||||
**Validation Lanes**: fast-feedback, confidence, focused browser smoke
|
||||
**Target Platform**: Laravel admin platform under `apps/platform`
|
||||
**Project Type**: monorepo with Laravel platform app plus separate website; this feature is platform-only
|
||||
**Performance Goals**: Keep register row derivation bounded to the already scoped workspace/environment query; avoid broad unscoped artifact/run lookups
|
||||
**Constraints**: No fake links, no legacy `/admin/t` URLs, no evidence payload duplication, no lifecycle mutation from the register, no new assets unless repo conventions prove unavoidable
|
||||
**Scale/Scope**: Narrow operator UI productization over the existing Decision Register page and builder
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed operator-facing Decision Register surface; possible minimal FindingException detail proof-label polish only if needed.
|
||||
- **Native vs custom classification summary**: native Filament page/table/infolist patterns. Do not introduce custom CSS, local card systems, custom assets, or ad-hoc button systems.
|
||||
- **Shared-family relevance**: proof/evidence viewers, stored report viewers, operation links, canonical navigation context, read-only governance register.
|
||||
- **State layers in scope**: page, table row projection, URL-query context for existing detail handoff.
|
||||
- **Audience modes in scope**: operator-MSP.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: decision-first register row; diagnostics and raw proof remain on existing detail/artifact/run pages.
|
||||
- **Raw/support gating plan**: raw JSON, fingerprints, provider payload details, and unsupported metadata remain secondary on detail/artifact pages.
|
||||
- **One-primary-action / duplicate-truth control**: existing row-to-FindingException detail handoff remains the primary inspect path. Proof and operation links are compact secondary affordances only when real and authorized.
|
||||
- **Handling modes by drift class or surface**: report-only for unsupported/loose references; show truthful missing/unavailable state instead of inventing a URL.
|
||||
- **Repository-signal treatment**: review-mandatory for any proposal to add a route, migration, asset, or new link resolver abstraction.
|
||||
- **Special surface test profiles**: standard-native-filament plus global-context-shell for register routing and tenant/workspace context.
|
||||
- **Required tests or manual smoke**: functional-core, state-contract, isolation, legacy route guard, manual browser smoke.
|
||||
- **Exception path and spread control**: none planned.
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: `DecisionRegister`, `GovernanceDecisionRegisterBuilder`, `FindingExceptionResource`, `ViewFindingException`, `FindingExceptionEvidenceReference`, `EvidenceSnapshotResource`, `StoredReportResource`, `OperationRunLinks`, `OperationRunUrl`, `CanonicalNavigationContext`, and existing governance/findings/evidence/operation tests.
|
||||
- **Shared abstractions reused**: existing builder, Filament resource `getUrl()` methods, `WorkspaceScopedTenantRoutes`, tenant-owned record authorization, `OperationRunLinks::tenantlessView()` / `OperationRunUrl`, existing policies.
|
||||
- **New abstraction introduced? why?**: none planned. If implementation proves a helper is needed, it must be a small internal helper with a proportionality note and no new registry/framework.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: Existing resources and link helpers are sufficient for canonical destination URLs and access checks. The existing builder/page is insufficient because it exposes count/missing proof state but no safe link metadata.
|
||||
- **Bounded deviation / spread control**: Any derived link metadata must remain read-only, local to the register projection, and backed by scoped queries from the current row.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, link-only.
|
||||
- **Central contract reused**: `OperationRunLinks` and, where locally established, `App\Support\OpsUx\OperationRunUrl`.
|
||||
- **Delegated UX behaviors**: run link, tenant/workspace-safe URL resolution.
|
||||
- **Surface-owned behavior kept local**: compact secondary operation link display or truthful missing/unavailable state.
|
||||
- **Queued DB-notification policy**: N/A.
|
||||
- **Terminal notification path**: N/A.
|
||||
- **Exception path**: none.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no.
|
||||
- **Provider-owned seams**: existing artifact/report payload pages only.
|
||||
- **Platform-core seams**: Decision Register row projection, proof/evidence/report/operation wording, scope-safe route generation.
|
||||
- **Neutral platform terms / contracts preserved**: proof, evidence, linked evidence, stored report, operation, workspace, environment.
|
||||
- **Retained provider-specific semantics and why**: Existing artifact pages may display provider-specific details because they already own artifact truth. The register must not copy or summarize provider payload detail into platform-core decision truth.
|
||||
- **Bounded extraction or follow-up path**: none.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before implementation. Re-check after design and before close-out.*
|
||||
|
||||
- **Inventory-first**: Pass. This feature links existing proof/artifact/run truth and does not change inventory semantics.
|
||||
- **Read/write separation**: Pass. Register changes are read-only. Existing lifecycle actions remain on queue/detail surfaces with their current confirmation, authorization, and audit paths.
|
||||
- **Graph contract path**: Pass. No provider/Graph calls are introduced.
|
||||
- **Deterministic capabilities**: Pass. Existing capabilities remain the gate; new tests must prove rendering and destination denial.
|
||||
- **RBAC-UX**: Pass if implementation preserves non-member/out-of-scope 404, member-missing-capability destination denial, and no UI-only authorization.
|
||||
- **Workspace isolation**: Pass if links resolve only from the row's workspace and destination policies remain authoritative.
|
||||
- **Tenant/environment isolation**: Pass if artifacts/runs are resolved only within the row's managed environment and no cross-environment URL is emitted.
|
||||
- **Run observability / OperationRun UX**: Pass. This feature deep-links to existing runs only; it does not start, complete, dedupe, or mutate runs.
|
||||
- **Data minimization**: Pass. No payloads are copied into the register row projection.
|
||||
- **TEST-GOV-001**: Pass with focused unit, feature, isolation, link guard, lifecycle regression, and browser smoke coverage.
|
||||
- **PROP-001 / BLOAT-001**: Pass. No new persisted truth, table, enum family, registry, provider seam, or UI taxonomy is planned.
|
||||
- **ABSTR-001 / LAYER-001**: Pass if implementation stays in existing builder/page or a small local helper only when duplication or security need proves it.
|
||||
- **PERSIST-001 / STATE-001**: Pass. Link states are presentation-only derived metadata.
|
||||
- **XCUT-001**: Pass. Existing resource URL helpers, policies, and OperationRun link helpers are reused.
|
||||
- **PROV-001**: Pass. Provider-specific semantics do not enter platform-core register logic.
|
||||
- **UI-FIL-001 / UI-HARD-001**: Pass if native Filament actions/columns are used, the row has one primary inspect path, and lifecycle/destructive actions stay out of the register.
|
||||
|
||||
## Test Governance Check
|
||||
|
||||
- **Test purpose / classification by changed surface**: Unit tests for builder projection; feature tests for Filament page rendering, authorization, canonical URLs, and boundary behavior; browser smoke for the final operator flow.
|
||||
- **Affected validation lanes**: fast-feedback, confidence, browser.
|
||||
- **Why this lane mix is the narrowest sufficient proof**: Builder unit tests catch projection mistakes cheaply. Feature tests prove URL/capability/scope behavior. Browser smoke catches Filament UI regressions in the actual table surface.
|
||||
- **Narrowest proving command(s)**: Use the focused commands listed in the Validation Commands section.
|
||||
- **Fixture / helper / factory / seed / context cost risks**: Moderate. Tests need workspaces, managed environments, scoped users/capabilities, finding exceptions, evidence references, optional evidence snapshots/stored reports, and optional operation runs.
|
||||
- **Expensive defaults or shared helper growth introduced?**: no. Any fixture helpers should remain feature-local or opt-in.
|
||||
- **Heavy-family additions, promotions, or visibility changes**: none.
|
||||
- **Surface-class relief / special coverage rule**: standard-native-filament and global-context-shell.
|
||||
- **Closing validation and reviewer handoff**: Re-run focused Decision Register, FindingException lifecycle, evidence/artifact, and OperationRun link guard lanes; run `git diff --check`; document browser smoke pass or reason not run.
|
||||
- **Budget / baseline / trend follow-up**: none.
|
||||
- **Review-stop questions**: Are any links inferred from loose text? Did any URL contain `/admin/t`? Did any cross-scope reference leak? Did any lifecycle action move into the register? Did any payload get duplicated?
|
||||
- **Escalation path**: split or reject if implementation needs persistence, a new route family, a link registry, or stored proof payloads.
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
|
||||
- **Why no dedicated follow-up spec is needed**: This is already the narrow proof-link follow-up selected by Spec 306; review-pack/customer-safe work remains separate follow-up candidates.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/307-decision-register-evidence-operationrun-link-polish/
|
||||
├── checklists/
|
||||
│ └── requirements.md
|
||||
├── plan.md
|
||||
├── spec.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Future implementation paths to inspect/edit
|
||||
|
||||
```text
|
||||
apps/platform/app/Filament/Pages/Governance/DecisionRegister.php
|
||||
apps/platform/resources/views/filament/pages/governance/decision-register.blade.php
|
||||
apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php
|
||||
apps/platform/app/Filament/Resources/FindingExceptionResource.php
|
||||
apps/platform/app/Filament/Resources/FindingExceptionResource/Pages/ViewFindingException.php
|
||||
apps/platform/app/Models/FindingException.php
|
||||
apps/platform/app/Models/FindingExceptionDecision.php
|
||||
apps/platform/app/Models/FindingExceptionEvidenceReference.php
|
||||
apps/platform/app/Models/EvidenceSnapshot.php
|
||||
apps/platform/app/Models/StoredReport.php
|
||||
apps/platform/app/Models/OperationRun.php
|
||||
apps/platform/app/Support/OperationRunLinks.php
|
||||
apps/platform/app/Support/OpsUx/OperationRunUrl.php
|
||||
apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php
|
||||
apps/platform/tests/Feature/Governance/DecisionRegisterPageTest.php
|
||||
apps/platform/tests/Feature/Governance/DecisionRegisterAuthorizationTest.php
|
||||
apps/platform/tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php
|
||||
apps/platform/tests/Feature/Findings/FindingExceptionDecisionRegisterBoundariesTest.php
|
||||
apps/platform/tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php
|
||||
apps/platform/tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php
|
||||
apps/platform/tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php
|
||||
```
|
||||
|
||||
**Structure Decision**: Keep the feature within existing platform app structure. The prep phase updates only the spec directory. Runtime implementation, when requested later, should use the files above and avoid new base directories.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
| Violation | Why Needed | Simpler Alternative Rejected Because |
|
||||
|-----------|------------|-------------------------------------|
|
||||
| None | N/A | N/A |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **Current operator problem**: The register shows decisions but not enough direct proof/run affordance for confident operator inspection.
|
||||
- **Existing structure is insufficient because**: Current proof display is count/missing-copy oriented and does not expose safe destination metadata for real proof artifacts or operation runs.
|
||||
- **Narrowest correct implementation**: Add derived link metadata to the existing builder/page, with safe fallbacks to detail or missing states.
|
||||
- **Ownership cost created**: Focused tests and small row/UI contract maintenance.
|
||||
- **Alternative intentionally rejected**: New decision/proof tables, route families, payload storage, workflow engine, OperationRun lifecycle integration, and customer-safe export.
|
||||
- **Release truth**: Current-release productization polish over already implemented Decision Register truth.
|
||||
|
||||
## Phase 0: Repo Verification Findings
|
||||
|
||||
- `GovernanceDecisionRegisterBuilder` already scopes rows by workspace and visible managed environments, and returns register row fields without proof link metadata.
|
||||
- `DecisionRegister` already renders a Filament table at `governance/decisions`, keeps detail handoff through `FindingExceptionResource::getUrl('view', ...)`, and shows proof count from `evidence_summary.reference_count`.
|
||||
- `FindingExceptionEvidenceReference` exists but uses `source_type` and nullable string `source_id`; these are not guaranteed local foreign keys. Implementation must resolve only known, real, scoped artifact IDs and otherwise avoid direct links.
|
||||
- `EvidenceSnapshotResource` and `StoredReportResource` provide canonical existing views with destination authorization.
|
||||
- `EvidenceSnapshot` can reference `operation_run_id`. `StoredReport` has no direct `operation_run_id` field in the inspected model.
|
||||
- `OperationRunLinks` / `OperationRunUrl` provide canonical run URLs under workspace context; implementation must not build operation URLs manually.
|
||||
- `FindingExceptionResource`, `EvidenceSnapshotResource`, and `StoredReportResource` are not globally searchable, so Filament global-search Edit/View-page hard rule is not newly triggered.
|
||||
- Filament panel provider registration remains in `apps/platform/bootstrap/providers.php`; this feature does not add a panel or provider.
|
||||
|
||||
## Phase 1: Technical Approach
|
||||
|
||||
1. Extend builder tests first for proof counts, no proof, single resolvable artifact, multiple references, stored report truth/no fake link, real/missing OperationRun links, and no legacy URLs.
|
||||
2. Extend `GovernanceDecisionRegisterBuilder` to eager-load scoped evidence references only where needed and derive a compact proof/run metadata projection.
|
||||
3. Resolve evidence/report links only from known real references:
|
||||
- one same-scope `EvidenceSnapshot` reference -> `EvidenceSnapshotResource::getUrl('view', ...)`
|
||||
- one same-scope `StoredReport` reference -> `StoredReportResource::getUrl('view', ...)`
|
||||
- multiple references or unsupported source -> existing FindingException detail proof handoff or truthful unavailable/missing state
|
||||
4. Resolve operation links only from real same-scope `OperationRun` relationships, such as `EvidenceSnapshot.operation_run_id`, or other repo-real run references discovered during implementation.
|
||||
5. Render compact proof/run affordances in the existing Decision Register UI while preserving the existing primary detail handoff.
|
||||
6. Add feature/isolation tests for rendered links, missing states, legacy URL guard, cross-workspace denial, cross-environment denial, and read-only lifecycle ownership.
|
||||
7. Keep existing lifecycle/audit tests green.
|
||||
|
||||
## Data Model / Contract Decisions
|
||||
|
||||
- No new table or migration is expected.
|
||||
- No evidence/report payloads are copied into decision rows.
|
||||
- Derived proof metadata may include:
|
||||
- `proof_count`
|
||||
- `proof_state`
|
||||
- `proof_label`
|
||||
- `proof_url`
|
||||
- `proof_url_label`
|
||||
- `operation_run_state`
|
||||
- `operation_run_url`
|
||||
- `operation_run_label`
|
||||
- `unavailable_reason`
|
||||
- Allowed proof states: `linked_evidence`, `linked_report`, `linked_detail_section`, `unavailable`, `not_linked`.
|
||||
- Allowed operation states: `linked_run`, `run_not_available`, `not_linked`.
|
||||
- URLs are nullable and present only when scoped, canonical, and access-safe.
|
||||
|
||||
## Filament / Laravel Compliance Plan
|
||||
|
||||
- **Livewire v4.0+ compliance**: The project runs Livewire 4.1.4 with Filament 5.2.1. The implementation must not introduce Livewire v3 references or Filament v3/v4 APIs.
|
||||
- **Provider registration**: No provider changes are planned. Laravel 12 panel providers remain registered in `apps/platform/bootstrap/providers.php`, not `bootstrap/app.php`.
|
||||
- **Global search**: No globally searchable resource is added. Existing relevant resources are globally searchable disabled, so no new Edit/View-page global-search requirement is introduced.
|
||||
- **Destructive actions**: No destructive or lifecycle actions are added to the register. Existing FindingException detail actions retain their current confirmation and authorization behavior.
|
||||
- **Asset strategy**: No new assets are expected. If registered assets become unavoidable, deployment must include `cd apps/platform && php artisan filament:assets`; otherwise this feature remains asset-neutral.
|
||||
- **Testing plan**: Test builder metadata, Filament page rendering, action/link visibility, destination authorization, cross-workspace/environment denial, lifecycle action absence, and browser smoke.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
Primary Decision Register lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php \
|
||||
tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php \
|
||||
tests/Feature/Governance/DecisionRegisterPageTest.php \
|
||||
tests/Feature/Governance/DecisionRegisterAuthorizationTest.php \
|
||||
tests/Feature/Governance/GovernanceInboxPageTest.php \
|
||||
tests/Feature/Governance/GovernanceInboxAuthorizationTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDetailDecisionSummaryTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDecisionRegisterBoundariesTest.php
|
||||
```
|
||||
|
||||
FindingException lifecycle/audit lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Unit/Findings/FindingExceptionDecisionTest.php \
|
||||
tests/Feature/Findings/FindingExceptionWorkflowTest.php \
|
||||
tests/Feature/Findings/FindingExceptionRenewalTest.php \
|
||||
tests/Feature/Findings/FindingExceptionRevocationTest.php
|
||||
```
|
||||
|
||||
Evidence/review/artifact lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Feature/Evidence/EvidenceSnapshotResourceTest.php \
|
||||
tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewRegisterRbacTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php \
|
||||
tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php
|
||||
```
|
||||
|
||||
OperationRun/link guard lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php \
|
||||
tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php \
|
||||
tests/Feature/ProviderConnections/LegacyRedirectTest.php \
|
||||
tests/Feature/RequiredPermissions/RequiredPermissionsLegacyRouteTest.php \
|
||||
tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php \
|
||||
tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php
|
||||
```
|
||||
|
||||
Whitespace:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Browser smoke:
|
||||
|
||||
1. Open Governance > Decisions.
|
||||
2. Confirm rows render.
|
||||
3. Confirm a decision with proof shows a proof affordance.
|
||||
4. Confirm a decision without proof shows `No linked proof` or equivalent.
|
||||
5. Open a proof link where available.
|
||||
6. Open an OperationRun link where available.
|
||||
7. Confirm row detail handoff still opens FindingException detail.
|
||||
8. Confirm no lifecycle actions moved into the register.
|
||||
9. Confirm no `/admin/t` URL appears.
|
||||
|
||||
## Documentation Impact
|
||||
|
||||
No product docs are changed during prep. During implementation close-out, update `docs/product/implementation-ledger.md` and/or `docs/product/spec-candidates.md` only if runtime behavior is completed and product truth changes. Do not mark the Decision Register as sellable unless customer-safe/review-pack gaps are separately solved.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- None blocking preparation.
|
||||
- Implementation must verify exact source-type strings and ID semantics before deciding whether an evidence reference can be resolved directly to `EvidenceSnapshot` or `StoredReport`.
|
||||
- Implementation must document if OperationRun links are not repo-real for any decision path beyond `EvidenceSnapshot.operation_run_id`.
|
||||
@ -0,0 +1,404 @@
|
||||
# Feature Specification: Decision Register Evidence / OperationRun Link Polish
|
||||
|
||||
**Feature Branch**: `307-decision-register-evidence-operationrun-link-polish`
|
||||
**Created**: 2026-05-15
|
||||
**Status**: Draft
|
||||
**Input**: User-provided full draft for Spec 307, promoted from the manual `decision-register-evidence-operationrun-link-polish` candidate after Spec 306 reconciliation.
|
||||
|
||||
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
|
||||
|
||||
- **Problem**: The operator Decision Register exists and is backed by existing `FindingException` / `FindingExceptionDecision` truth, but proof, evidence/report references, and related `OperationRun` context are not visible enough from the register path.
|
||||
- **Today's failure**: Operators can see a governance decision, but must often open the detail page or reconstruct context elsewhere to learn which proof, report, or execution record supports the decision. A count without a truthful affordance weakens confidence in the governance-of-record story.
|
||||
- **User-visible improvement**: Register rows show a calm proof state, direct evidence/report or aggregate proof links only where repo-real and authorized, optional canonical operation links only where real run references exist, and clear missing states when no proof/run link exists.
|
||||
- **Smallest enterprise-capable version**: Extend the existing read-only Decision Register row projection and Filament table display with safe derived link metadata. Reuse existing artifact resources, existing `FindingException` detail handoff, and existing OperationRun link helpers. Do not add persistence, lifecycle actions, or a new workflow engine.
|
||||
- **Explicit non-goals**: No new `governance_decisions` table, no approval engine, no inline register approve/reject/renew/revoke actions, no OperationRun lifecycle controller, no evidence payload storage, no Review Pack redesign, no customer-facing register/export, no notification system, no provider/Graph changes, and no broad navigation redesign.
|
||||
- **Permanent complexity imported**: Low. The slice may add derived array metadata to the existing builder, focused tests, and compact table/link rendering. It must not add a new model, table, enum family, DTO class, presenter framework, route family, or asset bundle.
|
||||
- **Why now**: `docs/product/roadmap.md`, `docs/product/implementation-ledger.md`, and `docs/product/spec-candidates.md` rank this as the first current manual promotion. Spec 306 specifically classifies the current register as partial productization and names this narrow proof-link follow-up as the next step.
|
||||
- **Why not local**: The current local count-only display does not answer the operator's next question: "What proof supports this decision, and what execution or evidence record should I inspect next?" The smallest fix belongs in the existing register builder/page, not in a new workflow or artifact system.
|
||||
- **Approval class**: Workflow Compression
|
||||
- **Red flags triggered**: Cross-cutting evidence/report and OperationRun link affordances. Defense: the slice reuses existing helpers and destination authorization, introduces no new persisted truth, and forbids fake links.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
|
||||
- **Decision**: approve
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: canonical-view
|
||||
- **Primary Routes**:
|
||||
- Existing `/admin/governance/decisions` Decision Register page.
|
||||
- Existing workspace/environment scoped `FindingExceptionResource` view route for detail handoff and aggregate proof fallback.
|
||||
- Existing `EvidenceSnapshotResource` view route when a referenced evidence snapshot can be resolved safely.
|
||||
- Existing `StoredReportResource` view route when a referenced stored report can be resolved safely.
|
||||
- Existing canonical `admin.operations.view` route through `OperationRunLinks` / `OperationRunUrl` when a real operation run reference can be resolved safely.
|
||||
- **Data Ownership**:
|
||||
- `FindingException`, `FindingExceptionDecision`, and `FindingExceptionEvidenceReference` remain decision truth.
|
||||
- `EvidenceSnapshot` and `StoredReport` remain artifact truth.
|
||||
- `OperationRun` remains execution truth.
|
||||
- The register stores no copied payloads and introduces no new persisted decision or proof model.
|
||||
- **RBAC**:
|
||||
- Workspace membership is the first boundary.
|
||||
- Register visibility continues to use `Capabilities::FINDING_EXCEPTION_VIEW`.
|
||||
- Destination pages keep their own policies/capabilities: evidence, stored reports, finding exceptions, and operation runs are server-side authorized.
|
||||
- Non-member or out-of-scope workspace/environment targets remain deny-as-not-found (`404`).
|
||||
- Member-but-missing-capability destination access remains `403` where existing policies define capability denial.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when tenant-context is active**: Existing Decision Register environment prefilter behavior is preserved. Link polish must not change the default open-decision register or recently-closed redirect behavior.
|
||||
- **Explicit entitlement checks preventing cross-tenant leakage**: Proof/run link resolution must begin from the already scoped `FindingException` row and only resolve artifacts/runs within the same workspace and environment. Hidden destinations must produce a disabled/unavailable state or no URL, not a cross-scope URL.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse *(mandatory)*
|
||||
|
||||
- **Cross-cutting feature?**: yes
|
||||
- **Interaction class(es)**: action links, evidence/report viewers, operation links, navigation continuity, table proof state, missing-state copy.
|
||||
- **Systems touched**: `DecisionRegister`, `GovernanceDecisionRegisterBuilder`, `FindingExceptionResource`, `ViewFindingException`, `FindingExceptionEvidenceReference`, `EvidenceSnapshotResource`, `StoredReportResource`, `OperationRunLinks`, `OperationRunUrl`, `CanonicalNavigationContext`, and focused governance/findings/evidence/operation tests.
|
||||
- **Existing pattern(s) to extend**: existing Filament resources and resource `getUrl()` methods, existing `WorkspaceScopedTenantRoutes`, existing tenant-owned record authorization, existing `OperationRunLinks::tenantlessView()`, existing FindingException detail handoff.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: Reuse the existing builder rather than adding a new decision presenter. Reuse existing resource URL helpers and OperationRun link helpers rather than constructing URLs locally.
|
||||
- **Why the existing shared path is sufficient or insufficient**: Existing destination pages and link helpers are sufficient for canonical links. The current register builder/page is insufficient because it exposes only proof count/missing copy and not safe link metadata.
|
||||
- **Allowed deviation and why**: A small derived link-metadata array inside the existing builder/page is allowed if it keeps the projection read-only and avoids a new class hierarchy.
|
||||
- **Consistency impact**: Link labels must stay calm: `View proof`, `View evidence`, `View report`, `View operation`, `No linked proof`, and `No operation linked`.
|
||||
- **Review focus**: Block fake links, raw URL concatenation, cross-workspace/environment URL generation, copied evidence payloads, and lifecycle actions on the register.
|
||||
|
||||
## OperationRun UX Impact *(mandatory)*
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: yes, link-only.
|
||||
- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks` and, where locally established, `App\Support\OpsUx\OperationRunUrl`.
|
||||
- **Delegated start/completion UX behaviors**: Only canonical `View operation` / `Open operation` URL resolution. No queued toast, no browser event, no DB notification, no dedupe/start failure messaging, and no status/outcome mutation.
|
||||
- **Local surface-owned behavior that remains**: The register may show an optional secondary operation link when a same-scope, authorized run exists. It may show `No operation linked` or omit the operation link when no real run exists.
|
||||
- **Queued DB-notification policy**: N/A
|
||||
- **Terminal notification path**: N/A
|
||||
- **Exception required?**: none
|
||||
|
||||
## Provider Boundary / Platform Core Check *(mandatory)*
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no
|
||||
- **Boundary classification**: N/A
|
||||
- **Seams affected**: Existing artifact metadata may contain provider-owned labels, but the register link logic must use platform-core terms: proof, evidence, report, operation, workspace, environment.
|
||||
- **Neutral platform terms preserved or introduced**: proof, evidence, stored report, operation, workspace, environment.
|
||||
- **Provider-specific semantics retained and why**: Existing artifact destination pages may display provider-specific report details because they already own that truth. The register must not promote provider payload detail into row-level decision truth.
|
||||
- **Why this does not deepen provider coupling accidentally**: Link resolution is based on repo-real models, IDs, scope, and existing resources, not provider-specific text or Graph payload fields.
|
||||
- **Follow-up path**: none for this slice.
|
||||
|
||||
## UI / Surface Guardrail Impact *(mandatory)*
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Decision Register page | yes | Native Filament table/page with existing Blade host only | proof links, operation links, read-only decision register | page, table row, URL-query context | no | Compact link polish over existing surface |
|
||||
| FindingException detail evidence section | maybe minimal | Native Filament infolist | aggregate proof fallback and unsupported reference display | detail | no | Only polish labels or anchors if needed |
|
||||
|
||||
Implementation intent: use Filament table columns/actions or existing linkable text primitives. Do not add local CSS, ad-hoc cards, custom button systems, hover affordance without a real URL, or new assets. Filament remains v5 with Livewire v4.
|
||||
|
||||
## Decision-First Surface Role *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Decision Register page | Primary Decision Surface | Operator chooses which governance decision to inspect next | decision state, owner, due context, proof state, optional operation link, `Open decision` | full decision history and evidence details stay on FindingException/artifact/run pages | Primary because it is the workspace register for follow-through | Answers "what needs proof inspection now?" without becoming the proof owner | Reduces searching across detail, evidence, reports, and operations |
|
||||
| FindingException detail page | Secondary Context | Operator validates proof and performs existing lifecycle actions | current decision summary, detail-owned actions, evidence references | artifact details, decision history, audit context | Secondary because it owns proof depth and lifecycle action | Preserves existing queue/detail ownership | Avoids duplicating lifecycle controls on the register |
|
||||
|
||||
## Audience-Aware Disclosure *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Decision Register page | operator-MSP | proof count/state, one proof affordance, optional operation link, existing next action | destination pages hold diagnostics | raw payloads, fingerprints, provider dumps, and JSON stay off rows | `Open decision` remains the primary row inspect action | artifact/run URLs hidden or disabled when unavailable or unauthorized | row states summarize availability only; artifact pages own details |
|
||||
| FindingException detail page | operator-MSP | decision and evidence-reference summary | decision history and evidence references | raw/support evidence remains secondary/collapsible where present | existing lifecycle action when allowed | unsupported references show truthful non-linked state | detail deepens one selected decision only |
|
||||
|
||||
## UI/UX Surface Classification *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Decision Register page | Utility / Workspace Decision | Read-only registry report | Open decision, then inspect proof or operation if needed | existing row click/detail handoff remains primary | allowed/current behavior preserved | compact proof/run links stay secondary | none | `/admin/governance/decisions` | existing FindingException view route | workspace, optional environment, register state | Decision register | proof availability and next inspection target | none |
|
||||
|
||||
## Operator Surface Contract *(mandatory)*
|
||||
|
||||
| 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Decision Register page | Workspace operator / MSP operator | Decide which decision proof or operation record to inspect next | Read-only decision register | What proof supports this decision, and where should I inspect next? | current decision row, proof count/state/link, optional operation link, missing proof/run copy | raw evidence summaries, artifact payloads, run diagnostics | decision status, validity/impact, proof availability, operation-link availability | none | Open decision | none |
|
||||
| FindingException detail page | Workspace operator / approver | Validate proof and use existing lifecycle controls | Detail / reviewable governance record | Why is this decision in this state and what action is allowed now? | decision summary, evidence references, existing header actions | deeper audit/evidence payloads and unsupported reference metadata | decision lifecycle, evidence availability, audit history | existing FindingException lifecycle only | renew/revoke or queue-owned approve/reject when already allowed | existing detail-owned destructive actions only, with confirmation |
|
||||
|
||||
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||
|
||||
- **New source of truth?**: no
|
||||
- **New persisted entity/table/artifact?**: no
|
||||
- **New abstraction?**: no new class or framework; only derived metadata on the existing builder/page if needed.
|
||||
- **New enum/state/reason family?**: no persisted family. Allowed derived link states are local projection strings only: `linked_evidence`, `linked_report`, `linked_detail_section`, `unavailable`, `not_linked`, `linked_run`, `run_not_available`.
|
||||
- **New cross-domain UI framework/taxonomy?**: no
|
||||
- **Current operator problem**: Operators need visible proof/run affordances for existing decisions without rebuilding decision truth.
|
||||
- **Existing structure is insufficient because**: The current register proof column shows a count or `No linked proof` but does not expose safe direct links where existing artifact/run truth is resolvable.
|
||||
- **Narrowest correct implementation**: Add read-only derived link metadata and compact rendering to the existing builder/page; fallback to the existing detail surface for aggregate or unsupported cases.
|
||||
- **Ownership cost**: Focused tests and a small builder/page extension. No migration, no new lifecycle owner, no new navigation family.
|
||||
- **Alternative intentionally rejected**: New decision table, proof storage, artifact-link service framework, operation lifecycle integration, and customer-safe export were rejected as over-scoped.
|
||||
- **Release truth**: current-release productization polish over existing operator truth.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes a pre-production environment. No legacy `/admin/t` route, compatibility alias, migration shim, or stale tenant-panel URL is allowed.
|
||||
|
||||
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||
|
||||
- **Test purpose / classification**: Unit, Feature, and one Browser smoke recommendation because the feature changes operator UI/link behavior.
|
||||
- **Validation lane(s)**: fast-feedback and confidence for focused Pest coverage; browser only for the manual/in-app smoke path.
|
||||
- **Why this classification and these lanes are sufficient**: Builder unit tests prove derived link states cheaply. Feature tests prove rendering, URL canonicality, authorization, and cross-scope denial. Browser smoke verifies the Filament table/link experience without creating a new browser family.
|
||||
- **New or expanded test families**: Existing `GovernanceDecisionRegisterBuilderTest`, `DecisionRegisterPageTest`, `DecisionRegisterAuthorizationTest`, `FindingExceptionDecisionRegisterNavigationTest`, `FindingExceptionDecisionRegisterBoundariesTest`, and existing evidence/operation link guard tests may be extended. No new heavy-governance family.
|
||||
- **Fixture / helper cost impact**: Moderate, feature-local. Tests need scoped workspaces/environments, FindingException evidence references, optional EvidenceSnapshot/StoredReport/OperationRun records, and negative cross-scope fixtures. Helpers should stay opt-in.
|
||||
- **Heavy-family visibility / justification**: none.
|
||||
- **Special surface test profile**: global-context-shell plus standard-native-filament.
|
||||
- **Reviewer handoff**: Confirm no lifecycle actions moved into the register, no fake links appear from loose text, `/admin/t` is absent, and destination policies still enforce access.
|
||||
- **Budget / baseline / trend impact**: low feature-local increase only.
|
||||
- **Escalation needed**: none.
|
||||
- **Active feature PR close-out entry**: Guardrail / Smoke Coverage.
|
||||
- **Planned validation commands**: see Validation Commands.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - See truthful proof state on each decision row (Priority: P1)
|
||||
|
||||
As a workspace operator, I want the Decision Register to show whether a decision has linked proof so I know whether the visible decision is supported before opening detail pages.
|
||||
|
||||
**Why this priority**: This is the minimum visible productization improvement and protects against false calmness.
|
||||
|
||||
**Independent Test**: Seed decisions with zero, one, and multiple evidence references and verify the register displays truthful count/state/copy without fake URLs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a visible decision has no evidence references, **When** the register renders, **Then** the proof column shows `No linked proof` or equivalent and no proof URL is emitted.
|
||||
2. **Given** a visible decision has evidence references, **When** the register renders, **Then** the proof count matches existing references and the row exposes either a safe proof link or detail-section handoff.
|
||||
3. **Given** multiple proof references exist, **When** the register renders, **Then** it uses one calm aggregate proof affordance instead of multiple noisy peer links.
|
||||
|
||||
### User Story 2 - Open real evidence/report/operation truth from the register (Priority: P1)
|
||||
|
||||
As a workspace operator, I want direct links to evidence snapshots, stored reports, or operations where they are real and authorized so I can inspect the proof trail without reconstructing it.
|
||||
|
||||
**Why this priority**: It closes the Spec 306 productization gap without changing lifecycle ownership.
|
||||
|
||||
**Independent Test**: Seed same-scope EvidenceSnapshot, StoredReport, and OperationRun references and verify generated links are canonical, authorized, and absent when references cannot be resolved.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** exactly one resolvable same-scope evidence snapshot reference exists, **When** the row renders, **Then** `View evidence` or `View proof` opens the existing EvidenceSnapshot view.
|
||||
2. **Given** exactly one resolvable same-scope stored-report reference exists, **When** the row renders, **Then** `View report` opens the existing StoredReport view.
|
||||
3. **Given** a real same-scope OperationRun reference exists through the decision source or linked artifact, **When** the row renders, **Then** `View operation` uses the canonical OperationRun helper URL.
|
||||
4. **Given** a source reference cannot be resolved to a real model and authorized route, **When** the row renders, **Then** the row shows a truthful non-linked state.
|
||||
|
||||
### User Story 3 - Preserve scope, authorization, and lifecycle boundaries (Priority: P1)
|
||||
|
||||
As a workspace operator, I need proof and operation links to respect workspace/environment/RBAC boundaries and keep lifecycle actions on existing surfaces.
|
||||
|
||||
**Why this priority**: Proof links are trust features; cross-scope leakage or lifecycle drift would be a security and product-truth regression.
|
||||
|
||||
**Independent Test**: Attempt cross-workspace and cross-environment link access and verify generated URLs are absent or destination access denies as existing policies define.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a referenced artifact belongs to another workspace, **When** the register builds rows, **Then** no URL for that artifact is rendered and direct access remains denied.
|
||||
2. **Given** a referenced operation belongs to another environment, **When** the register builds rows, **Then** no operation URL is rendered and direct access remains denied.
|
||||
3. **Given** the operator opens the register, **When** they inspect row actions, **Then** approve, reject, renew, revoke, and closure actions are not introduced on the register.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Evidence reference has a `source_type` but no `source_id`.
|
||||
- Evidence reference `source_id` is a loose external key rather than a numeric local artifact ID.
|
||||
- Evidence reference points to a deleted or inaccessible artifact.
|
||||
- Stored report exists but the user lacks the report-family capability.
|
||||
- Evidence snapshot exists but the current user cannot view evidence for that environment.
|
||||
- OperationRun exists but the user lacks workspace membership, environment entitlement, or the run-required capability.
|
||||
- Multiple evidence references include mixed resolvable and unsupported sources.
|
||||
- Query parameters or generated links must not contain legacy `/admin/t`.
|
||||
- Recently closed decisions keep proof affordances but do not become lifecycle-action surfaces.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: Decision Register rows MUST expose a truthful proof state for each visible decision: linked proof, aggregate proof on detail, unavailable proof, or no linked proof.
|
||||
- **FR-002**: The register MUST NOT render a proof URL unless a real same-scope model/id/context exists and the current user can safely attempt the destination.
|
||||
- **FR-003**: A single resolvable `EvidenceSnapshot` reference MUST link to the existing EvidenceSnapshot view using the resource URL helper and workspace/environment-scoped route semantics.
|
||||
- **FR-004**: A single resolvable `StoredReport` reference MUST link to the existing StoredReport view using the resource URL helper and stored-report capability semantics.
|
||||
- **FR-005**: Multiple evidence/report references SHOULD link to the existing FindingException detail proof section or equivalent safe aggregate detail target rather than rendering a noisy multi-link cluster.
|
||||
- **FR-006**: Unsupported evidence references MUST render as truthful non-linked proof availability and MUST NOT break the row or detail rendering.
|
||||
- **FR-007**: OperationRun links MUST appear only when a real same-scope OperationRun reference exists directly or through a repo-real linked artifact/source.
|
||||
- **FR-008**: OperationRun URLs MUST use `OperationRunLinks` or `OperationRunUrl`; raw string-concatenated operation URLs are forbidden.
|
||||
- **FR-009**: Generated proof/report/run URLs MUST be canonical workspace/environment/admin URLs and MUST NOT contain `/admin/t`.
|
||||
- **FR-010**: Link resolution MUST remain scoped to the current workspace and visible environment set before counts and labels are derived.
|
||||
- **FR-011**: Destination pages MUST keep their own server-side authorization; register link visibility is not a security boundary.
|
||||
- **FR-012**: The existing Decision Register row to FindingException detail handoff MUST remain intact and must continue to carry navigation context.
|
||||
- **FR-013**: Approval, rejection, renewal, revocation, and closure actions MUST remain on existing queue/detail surfaces and MUST NOT be added to the register.
|
||||
- **FR-014**: Evidence/report payloads, stored-report payloads, and OperationRun lifecycle truth MUST NOT be copied into register rows or decision records.
|
||||
- **FR-015**: Any derived builder metadata MUST remain read-only, deterministic, testable, and bounded to already scoped queries.
|
||||
|
||||
### Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: UI copy must stay calm and non-alarming for missing references: `No linked proof`, `No operation linked`, `Proof available on detail`, or equivalent.
|
||||
- **NFR-002**: The register must remain scanable with one primary row inspect model and at most compact secondary proof/run affordances.
|
||||
- **NFR-003**: No local UI styling system, new assets, or custom CSS framework may be introduced.
|
||||
- **NFR-004**: No Microsoft Graph/provider boundary changes may be introduced.
|
||||
- **NFR-005**: No migration is expected; any proposed migration must prove existing relations cannot safely support the narrow link behavior.
|
||||
|
||||
## 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 |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Decision Register page | `apps/platform/app/Filament/Pages/Governance/DecisionRegister.php` | existing scope/filter controls only | existing row/detail handoff remains primary | proof link, optional operation link only when real and authorized | none | existing filter reset/current decision CTA behavior preserved | N/A | N/A | none, read-only | No lifecycle actions |
|
||||
| FindingException detail | `apps/platform/app/Filament/Resources/FindingExceptionResource.php`; `.../Pages/ViewFindingException.php` | existing return/renew/revoke actions preserved | N/A | N/A | none | existing | existing lifecycle actions preserved | N/A | existing service audit for mutations | Minimal evidence-label/link polish only if needed |
|
||||
|
||||
## Key Entities *(include if feature involves data)*
|
||||
|
||||
- **FindingException**: Current decision aggregate and register row source.
|
||||
- **FindingExceptionDecision**: Append-only decision history and current decision pointer.
|
||||
- **FindingExceptionEvidenceReference**: Existing proof-reference records with `source_type`, `source_id`, `source_fingerprint`, label, summary, and measured timestamp.
|
||||
- **EvidenceSnapshot**: Existing evidence artifact truth with workspace/environment scope and optional OperationRun relationship.
|
||||
- **StoredReport**: Existing retained report artifact truth with workspace/environment scope and report-family capability semantics.
|
||||
- **OperationRun**: Existing execution truth and canonical operation detail destination.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: A user can identify from the register whether a decision has linked proof without opening the detail page.
|
||||
- **SC-002**: Decisions with no proof render a truthful missing state and no proof URL.
|
||||
- **SC-003**: Resolvable same-scope evidence/report links from the register open existing canonical artifact views.
|
||||
- **SC-004**: Resolvable same-scope operation links from the register open the canonical OperationRun view and never emit `/admin/t`.
|
||||
- **SC-005**: Cross-workspace and cross-environment proof/run references do not leak URLs or data.
|
||||
- **SC-006**: Existing detail handoff and lifecycle ownership tests remain green.
|
||||
- **SC-007**: No new persistence, workflow engine, approval engine, OperationRun lifecycle behavior, provider integration, or asset bundle is introduced.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- **AC-001**: Proof affordance improved for real references.
|
||||
- **AC-002**: Missing proof is truthful and non-linked.
|
||||
- **AC-003**: Evidence links are canonical and scope-safe.
|
||||
- **AC-004**: Stored-report links appear only for repo-real, authorized stored-report references.
|
||||
- **AC-005**: OperationRun links appear only for real, authorized run references and use existing helpers.
|
||||
- **AC-006**: Evidence/report payloads and run lifecycle truth are not duplicated.
|
||||
- **AC-007**: Existing FindingException detail handoff still works.
|
||||
- **AC-008**: Lifecycle actions are not moved into the register.
|
||||
- **AC-009**: Workspace isolation is preserved.
|
||||
- **AC-010**: Environment isolation is preserved.
|
||||
- **AC-011**: Destination RBAC remains server-side enforced.
|
||||
- **AC-012**: Focused Decision Register, Governance Inbox, FindingException, artifact, and OperationRun link tests pass.
|
||||
- **AC-013**: `git diff --check` passes.
|
||||
- **AC-014**: Browser smoke passes or close-out documents why it was not run.
|
||||
- **AC-015**: No broad Decision Register rebuild occurs.
|
||||
|
||||
## Current Truth Boundary
|
||||
|
||||
- `GovernanceDecisionRegisterBuilder` currently derives rows from scoped `FindingException` records and current decisions.
|
||||
- `DecisionRegister` currently renders a native Filament table, proof count from `evidence_summary.reference_count`, and `No linked proof` when zero.
|
||||
- `FindingExceptionEvidenceReference` currently stores `source_type` and `source_id` as reference metadata, not a guaranteed foreign key.
|
||||
- `EvidenceSnapshotResource`, `StoredReportResource`, and canonical OperationRun pages exist with their own scope/authorization semantics.
|
||||
- `OperationRunPolicy` enforces workspace membership, environment entitlement, and run-specific capability where required.
|
||||
- The implementation must use repo-real names/fields if they differ from this spec and document any adjustment in implementation close-out.
|
||||
|
||||
## Validation Commands
|
||||
|
||||
Primary Decision Register lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php \
|
||||
tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php \
|
||||
tests/Feature/Governance/DecisionRegisterPageTest.php \
|
||||
tests/Feature/Governance/DecisionRegisterAuthorizationTest.php \
|
||||
tests/Feature/Governance/GovernanceInboxPageTest.php \
|
||||
tests/Feature/Governance/GovernanceInboxAuthorizationTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDetailDecisionSummaryTest.php \
|
||||
tests/Feature/Findings/FindingExceptionDecisionRegisterBoundariesTest.php
|
||||
```
|
||||
|
||||
FindingException lifecycle/audit lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Unit/Findings/FindingExceptionDecisionTest.php \
|
||||
tests/Feature/Findings/FindingExceptionWorkflowTest.php \
|
||||
tests/Feature/Findings/FindingExceptionRenewalTest.php \
|
||||
tests/Feature/Findings/FindingExceptionRevocationTest.php
|
||||
```
|
||||
|
||||
Evidence/review/artifact lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Feature/Evidence/EvidenceSnapshotResourceTest.php \
|
||||
tests/Feature/Evidence/EvidenceSnapshotAuditLogTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewAuditLogTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewRegisterTest.php \
|
||||
tests/Feature/EnvironmentReview/EnvironmentReviewRegisterRbacTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspacePageTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspaceAuthorizationTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspacePackAccessTest.php \
|
||||
tests/Feature/Reviews/CustomerReviewWorkspaceLaunchLinksTest.php \
|
||||
tests/Feature/Filament/GovernanceArtifacts/GovernanceArtifactDeepLinkContractTest.php
|
||||
```
|
||||
|
||||
OperationRun/link guard lane:
|
||||
|
||||
```bash
|
||||
cd apps/platform && ./vendor/bin/sail artisan test --compact \
|
||||
tests/Feature/Monitoring/OperationsDashboardDrillthroughTest.php \
|
||||
tests/Feature/Operations/LegacyRunRoutesNotFoundTest.php \
|
||||
tests/Feature/ProviderConnections/LegacyRedirectTest.php \
|
||||
tests/Feature/RequiredPermissions/RequiredPermissionsLegacyRouteTest.php \
|
||||
tests/Feature/Guards/ManagedEnvironmentCanonicalRouteContractTest.php \
|
||||
tests/Feature/Filament/PolicyVersionResolvedReferenceLinksTest.php
|
||||
```
|
||||
|
||||
Whitespace:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Browser smoke is recommended because this changes operator UI/link behavior:
|
||||
|
||||
1. Open Governance > Decisions.
|
||||
2. Confirm rows render.
|
||||
3. Confirm a decision with proof shows a proof affordance.
|
||||
4. Confirm a decision without proof shows a calm missing state.
|
||||
5. Open a proof link where available.
|
||||
6. Open an OperationRun link where available.
|
||||
7. Confirm row detail handoff still opens FindingException detail.
|
||||
8. Confirm no lifecycle actions moved into the register.
|
||||
9. Confirm no `/admin/t` URL appears.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- New decision persistence model.
|
||||
- New lifecycle actions.
|
||||
- Inline register approval/closure controls.
|
||||
- Evidence ingestion or payload storage.
|
||||
- Review Pack redesign or inclusion.
|
||||
- Customer-facing Decision Register.
|
||||
- Generic customer-safe export.
|
||||
- PSA/ITSM handoff.
|
||||
- Notification engine.
|
||||
- AI summaries.
|
||||
- Broad Governance Inbox or navigation redesign.
|
||||
- Provider/Graph changes.
|
||||
- Migrations unless repo verification proves a tiny metadata/index gap is unavoidable.
|
||||
- Assets unless required by existing build conventions.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The user-provided Spec 307 draft is the selected manual promotion.
|
||||
- Spec 265 is implemented/closed and must be treated as context only.
|
||||
- Spec 306 reconciliation is authoritative for the partial-productization classification.
|
||||
- Some existing `FindingExceptionEvidenceReference.source_id` values may be loose external identifiers; implementation must not treat them as local artifact IDs without verification.
|
||||
- StoredReport direct links are possible only for references resolvable to `StoredReport` records under existing report-family authorization.
|
||||
- OperationRun direct links are possible only for real run IDs or artifact relationships that resolve to `OperationRun` records under existing policy.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Risk: Fake OperationRun links**. Mitigation: only link real `OperationRun` records and use helper URLs.
|
||||
- **Risk: Evidence payload duplication**. Mitigation: link only; do not copy payloads into register rows.
|
||||
- **Risk: Register becomes noisy**. Mitigation: one proof affordance and optional compact operation link only.
|
||||
- **Risk: Cross-scope leak**. Mitigation: scope resolution from the row's workspace/environment and destination policy tests.
|
||||
- **Risk: Lifecycle drift**. Mitigation: no register mutations and run existing FindingException workflow tests.
|
||||
|
||||
## Follow-up Candidates
|
||||
|
||||
- `decision-register-review-pack-inclusion`
|
||||
- `decision-register-customer-safe-summary`
|
||||
- `decision-register-governance-inbox-cta-polish`
|
||||
- `decision-register-notification-follow-up`
|
||||
|
||||
Do not create a broad `Decision Register v1`.
|
||||
@ -0,0 +1,113 @@
|
||||
# Tasks: Decision Register Evidence / OperationRun Link Polish
|
||||
|
||||
**Input**: `/specs/307-decision-register-evidence-operationrun-link-polish/spec.md`, `/specs/307-decision-register-evidence-operationrun-link-polish/plan.md`
|
||||
**Prerequisites**: Existing Spec 265 and Spec 306 are context only; do not modify completed specs.
|
||||
**Scope**: Runtime implementation only when explicitly requested after prep. This task list is prepared now.
|
||||
|
||||
## Task Format
|
||||
|
||||
Each task uses `- [ ] T### [P?] [US?] Description with exact path`.
|
||||
|
||||
- `[P]` means the task can run in parallel after prerequisites.
|
||||
- `[US1]`, `[US2]`, `[US3]` map to the user stories in `spec.md`.
|
||||
- Tasks that touch the same file are intentionally ordered to avoid conflicts.
|
||||
|
||||
## Path Conventions
|
||||
|
||||
- Platform app: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform`
|
||||
- Feature spec: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/307-decision-register-evidence-operationrun-link-polish`
|
||||
|
||||
## Phase 1: Setup and Repo Verification
|
||||
|
||||
- [x] T001 Read `/Users/ahmeddarrazi/Documents/projects/wt-plattform/.specify/memory/constitution.md`, this spec, and this plan before runtime edits.
|
||||
- [x] T002 Confirm current branch is `307-decision-register-evidence-operationrun-link-polish` and working tree changes are expected.
|
||||
- [x] T003 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php` and record current row shape before editing.
|
||||
- [x] T004 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php` and current proof/detail handoff behavior before editing.
|
||||
- [x] T005 Inspect `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Models/FindingExceptionEvidenceReference.php` and its migration to confirm source-type/source-id semantics before resolving links.
|
||||
- [x] T006 Inspect artifact resources for canonical URLs and policies: `EvidenceSnapshotResource.php`, `StoredReportResource.php`, `OperationRunLinks.php`, and `OperationRunUrl.php`.
|
||||
- [x] T007 Confirm no migration is needed; if one appears necessary, stop and update spec/plan with proportionality proof before continuing.
|
||||
|
||||
## Phase 2: Tests First
|
||||
|
||||
- [x] T008 [P] [US1] Add builder coverage for proof count and `No linked proof` in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T009 [P] [US1] Add builder coverage for multiple evidence references falling back to detail proof handoff in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T010 [P] [US2] Add builder coverage for a single resolvable same-scope `EvidenceSnapshot` proof link when existing route support is available in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T011 [P] [US2] Add builder coverage for a single resolvable same-scope `StoredReport` proof link, or prove no fake stored-report link is emitted when the repo-real reference is unavailable, in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T012 [P] [US2] Add builder coverage for real OperationRun link metadata when a repo-real same-scope run reference exists in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T013 [P] [US2] Add builder coverage for missing OperationRun references producing no fake operation URL in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Unit/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilderTest.php`.
|
||||
- [x] T014 [P] [US3] Add page rendering coverage for proof link/missing proof copy and no `/admin/t` links in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Governance/DecisionRegisterPageTest.php`.
|
||||
- [x] T015 [P] [US3] Add authorization coverage for hidden/disabled unavailable proof/run links when the viewer lacks destination access in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Governance/DecisionRegisterAuthorizationTest.php`.
|
||||
- [x] T016 [P] [US3] Add or update boundary coverage for cross-workspace and cross-environment proof/run denial in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Findings/FindingExceptionDecisionRegisterBoundariesTest.php`.
|
||||
- [x] T017 [P] [US3] Add or update navigation coverage proving the existing Decision Register row-to-FindingException detail handoff still works in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Findings/FindingExceptionDecisionRegisterNavigationTest.php`.
|
||||
- [x] T018 [P] [US3] Add or update a feature assertion that approve/reject/renew/revoke/close lifecycle actions are not introduced on the Decision Register in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Governance/DecisionRegisterPageTest.php`.
|
||||
|
||||
## Phase 3: User Story 1 - Truthful Proof State
|
||||
|
||||
**Goal**: Each register row exposes a truthful proof count/state and calm missing copy without fake URLs.
|
||||
|
||||
**Independent Test**: Builder and page tests show zero, one, and multiple evidence states correctly.
|
||||
|
||||
- [x] T019 [US1] Eager-load scoped `evidenceReferences` where needed in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php` without broad unscoped queries.
|
||||
- [x] T020 [US1] Add derived proof metadata fields to register rows in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T021 [US1] Preserve existing `evidence_summary.reference_count` behavior or replace it only with an equivalent scoped count proven by tests in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T022 [US1] Render proof label/state and `No linked proof` copy in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php` using native Filament table patterns.
|
||||
- [x] T023 [US1] Ensure no proof URL is emitted for zero references, unsupported references, or unresolvable loose `source_id` values in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php`.
|
||||
|
||||
## Phase 4: User Story 2 - Real Evidence, Report, and Operation Links
|
||||
|
||||
**Goal**: Direct links appear only for real, same-scope, authorized proof artifacts and operation runs.
|
||||
|
||||
**Independent Test**: Single-artifact and OperationRun tests pass; unsupported references remain non-linked.
|
||||
|
||||
- [x] T024 [US2] Resolve one same-scope evidence snapshot reference to `EvidenceSnapshotResource::getUrl('view', ...)` in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T025 [US2] Resolve one same-scope stored report reference to `StoredReportResource::getUrl('view', ...)` only when report-family destination access is available in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T026 [US2] Route multiple proof references to the existing FindingException detail proof handoff or show a safe aggregate proof state in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T027 [US2] Resolve OperationRun links only from real same-scope run references and existing `OperationRunLinks` / `OperationRunUrl` helpers in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T028 [US2] Add compact secondary `View proof`, `View evidence`, `View report`, and `View operation` affordances where metadata provides safe URLs in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php`.
|
||||
- [x] T029 [US2] If the existing Blade host is involved, keep changes minimal and native-compatible in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/governance/decision-register.blade.php`.
|
||||
- [x] T030 [US2] If detail evidence labels need minimal support for aggregate handoff, update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/FindingExceptionResource.php` without adding lifecycle actions.
|
||||
|
||||
## Phase 5: User Story 3 - Isolation and Lifecycle Boundaries
|
||||
|
||||
**Goal**: Proof/run links preserve workspace/environment/RBAC boundaries and the register remains read-only.
|
||||
|
||||
**Independent Test**: Cross-scope denial, no legacy URLs, detail handoff, and lifecycle ownership assertions pass.
|
||||
|
||||
- [x] T031 [US3] Ensure all proof artifact lookup queries include current row workspace and managed-environment constraints in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T032 [US3] Ensure operation run lookup/link generation does not emit URLs for runs outside the row workspace/environment in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/GovernanceDecisions/GovernanceDecisionRegisterBuilder.php`.
|
||||
- [x] T033 [US3] Keep destination server-side authorization unchanged in `EvidenceSnapshotResource.php`, `StoredReportResource.php`, `FindingExceptionResource.php`, and operation run policies; do not replace destination authorization with UI visibility.
|
||||
- [x] T034 [US3] Confirm generated proof/report/run/detail URLs do not contain `/admin/t` in feature tests and implementation assertions where appropriate.
|
||||
- [x] T035 [US3] Confirm the Decision Register exposes no approve, reject, close, renew, revoke, delete, or restore action in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Governance/DecisionRegister.php`.
|
||||
|
||||
## Phase 6: Filament, Docs, and Validation
|
||||
|
||||
- [x] T036 Run primary Decision Register lane from `plan.md` and fix confirmed in-scope failures.
|
||||
- [x] T037 Run FindingException lifecycle/audit lane from `plan.md` and fix confirmed in-scope regressions.
|
||||
- [x] T038 Run evidence/review/artifact lane from `plan.md` and fix confirmed in-scope regressions.
|
||||
- [x] T039 Run OperationRun/link guard lane from `plan.md` and fix confirmed in-scope regressions.
|
||||
- [x] T040 Run `git diff --check` from `/Users/ahmeddarrazi/Documents/projects/wt-plattform`.
|
||||
- [x] T041 Run browser smoke for Governance > Decisions, proof/missing states, proof link, operation link, detail handoff, lifecycle absence, and no `/admin/t` URL.
|
||||
- [x] T042 Update product docs only if implementation is complete and product truth changes; likely files are `/Users/ahmeddarrazi/Documents/projects/wt-plattform/docs/product/implementation-ledger.md` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/docs/product/spec-candidates.md`.
|
||||
- [x] T043 Close out with changed files, migration status, proof/evidence link behavior, stored-report behavior, OperationRun behavior, missing-link behavior, tests, browser smoke, `git diff --check`, docs, and remaining gaps.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- T001-T007 must complete before runtime edits.
|
||||
- T008-T018 should be written before implementation tasks where practical.
|
||||
- T019-T023 are required before UI proof rendering is considered complete.
|
||||
- T024-T030 depend on repo-real artifact/run references verified in T005-T006.
|
||||
- T031-T035 must complete before validation.
|
||||
- T036-T043 are final validation and close-out tasks.
|
||||
|
||||
## Parallel Work Examples
|
||||
|
||||
- T008-T018 can be split across tests after T001-T007.
|
||||
- T024-T027 can be split from T028-T030 only after the proof/run metadata contract is stable.
|
||||
- T036-T039 can be run in parallel only if the local Sail/test environment supports it without DB conflicts.
|
||||
|
||||
## Notes
|
||||
|
||||
- If `FindingExceptionEvidenceReference.source_id` is loose text rather than a local artifact primary key, do not force direct links. Prefer detail proof handoff or unavailable state.
|
||||
- If stored-report references are not repo-real, prove that no fake stored-report link renders.
|
||||
- If OperationRun references are not repo-real for a path, prove that no fake operation link renders.
|
||||
- No implementation task may add a new decision table, workflow engine, approval action, evidence payload store, OperationRun lifecycle mutation, provider integration, or broad navigation redesign.
|
||||
Loading…
Reference in New Issue
Block a user