chore(platform): merge platform-dev into dev #302

Merged
ahmido merged 21 commits from platform-dev into dev 2026-04-29 20:53:45 +00:00
67 changed files with 1579 additions and 269 deletions
Showing only changes of commit b511b08371 - Show all commits

View File

@ -6,12 +6,14 @@
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\OperationCatalog;
use App\Support\OperationRunType;
use Illuminate\Console\Command;
class PurgeLegacyBaselineGapRuns extends Command
{
protected $signature = 'tenantpilot:baselines:purge-legacy-gap-runs
{--type=* : Limit cleanup to baseline_compare and/or baseline_capture runs}
{--type=* : Limit cleanup to baseline.compare and/or baseline.capture runs (legacy aliases also accepted)}
{--tenant=* : Limit cleanup to tenant ids or tenant external ids}
{--workspace=* : Limit cleanup to workspace ids}
{--limit=500 : Maximum candidate runs to inspect}
@ -99,21 +101,35 @@ public function handle(): int
*/
private function normalizedTypes(): array
{
$types = array_values(array_unique(array_filter(
$requestedTypes = array_values(array_unique(array_filter(
array_map(
static fn (mixed $type): ?string => is_string($type) && trim($type) !== '' ? trim($type) : null,
(array) $this->option('type'),
),
)));
if ($types === []) {
return ['baseline_compare', 'baseline_capture'];
$canonicalTypes = array_values(array_unique(array_filter(array_map(
static fn (string $type): ?string => match ($type) {
OperationRunType::BaselineCompare->value, 'baseline_compare' => OperationRunType::BaselineCompare->value,
OperationRunType::BaselineCapture->value, 'baseline_capture' => OperationRunType::BaselineCapture->value,
default => null,
},
$requestedTypes,
))));
if ($canonicalTypes === []) {
$canonicalTypes = [
OperationRunType::BaselineCompare->value,
OperationRunType::BaselineCapture->value,
];
}
return array_values(array_filter(
$types,
static fn (string $type): bool => in_array($type, ['baseline_compare', 'baseline_capture'], true),
));
return array_values(array_unique(array_merge(
...array_map(
static fn (string $type): array => OperationCatalog::rawValuesForCanonical($type),
$canonicalTypes,
),
)));
}
/**

View File

@ -16,6 +16,11 @@
use App\Support\Findings\FindingOutcomeSemantics;
use App\Support\Filament\TablePaginationProfiles;
use App\Support\ReviewPackStatus;
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome;
@ -57,6 +62,16 @@ class CustomerReviewWorkspace extends Page implements HasTable
protected string $view = 'filament.pages.reviews.customer-review-workspace';
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::RunLog, ActionSurfaceType::ReadOnlyRegistryReport)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header actions provide a single Clear filters action for the customer review workspace.')
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The customer review workspace remains scan-first and does not expose bulk actions.')
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state keeps exactly one Clear filters CTA when filters are active.')
->exempt(ActionSurfaceSlot::DetailHeader, 'Row navigation opens the latest published review detail instead of an inline canonical detail panel.');
}
public static function getNavigationGroup(): string
{
return __('localization.review.reporting');

View File

@ -308,8 +308,6 @@ public static function infolist(Schema $schema): Schema
? OperationRunLinks::tenantlessView((int) $record->current_operation_run_id, static::findingRunNavigationContext($record))
: null)
->openUrlInNewTab(),
TextEntry::make('acknowledged_at')->dateTime()->placeholder('—'),
TextEntry::make('acknowledged_by_user_id')->label('Acknowledged by')->placeholder('—'),
TextEntry::make('first_seen_at')->label('First seen')->dateTime()->placeholder('—'),
TextEntry::make('last_seen_at')->label('Last seen')->dateTime()->placeholder('—'),
TextEntry::make('times_seen')->label('Times seen')->placeholder('—'),
@ -1000,7 +998,6 @@ public static function table(Table $table): Table
if (! in_array((string) $record->status, [
Finding::STATUS_NEW,
Finding::STATUS_REOPENED,
Finding::STATUS_ACKNOWLEDGED,
], true)) {
$skippedCount++;
@ -1416,7 +1413,6 @@ public static function triageAction(): Actions\Action
->visible(fn (Finding $record): bool => in_array((string) $record->status, [
Finding::STATUS_NEW,
Finding::STATUS_REOPENED,
Finding::STATUS_ACKNOWLEDGED,
], true))
->action(function (Finding $record, FindingWorkflowService $workflow): void {
static::runWorkflowMutation(
@ -1441,7 +1437,6 @@ public static function startProgressAction(): Actions\Action
->color('info')
->visible(fn (Finding $record): bool => in_array((string) $record->status, [
Finding::STATUS_TRIAGED,
Finding::STATUS_ACKNOWLEDGED,
], true))
->action(function (Finding $record, FindingWorkflowService $workflow): void {
static::runWorkflowMutation(

View File

@ -171,7 +171,6 @@ protected function getHeaderActions(): array
if (! in_array((string) $finding->status, [
Finding::STATUS_NEW,
Finding::STATUS_REOPENED,
Finding::STATUS_ACKNOWLEDGED,
], true)) {
$skippedCount++;

View File

@ -57,11 +57,6 @@ public static function canAccess(): bool
&& $user->hasCapability(PlatformCapabilities::OPS_CONTROLS_MANAGE);
}
public function mount(): void
{
abort_unless(static::canAccess(), 403);
}
public function getHeader(): ?View
{
return view('filament.system.pages.ops.partials.controls-header', [

View File

@ -33,8 +33,6 @@ class Finding extends Model
public const string STATUS_NEW = 'new';
public const string STATUS_ACKNOWLEDGED = 'acknowledged';
public const string STATUS_TRIAGED = 'triaged';
public const string STATUS_IN_PROGRESS = 'in_progress';
@ -169,10 +167,7 @@ public static function terminalStatuses(): array
*/
public static function openStatusesForQuery(): array
{
return [
...self::openStatuses(),
self::STATUS_ACKNOWLEDGED,
];
return self::openStatuses();
}
/**
@ -295,10 +290,6 @@ public static function isReopenReason(?string $reason): bool
public static function canonicalizeStatus(?string $status): ?string
{
if ($status === self::STATUS_ACKNOWLEDGED) {
return self::STATUS_TRIAGED;
}
return $status;
}
@ -324,23 +315,6 @@ public function isRiskAccepted(): bool
return (string) $this->status === self::STATUS_RISK_ACCEPTED;
}
public function acknowledge(User $user): self
{
if ($this->status === self::STATUS_ACKNOWLEDGED) {
return $this;
}
$this->forceFill([
'status' => self::STATUS_ACKNOWLEDGED,
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
]);
$this->save();
return $this;
}
public function resolve(string $reason): self
{
$this->forceFill([

View File

@ -49,10 +49,7 @@ public function update(User $user, Finding $finding): Response|bool
public function triage(User $user, Finding $finding): Response|bool
{
return $this->canMutateWithAnyCapability($user, $finding, [
Capabilities::TENANT_FINDINGS_TRIAGE,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
]);
return $this->canMutateWithCapability($user, $finding, Capabilities::TENANT_FINDINGS_TRIAGE);
}
public function assign(User $user, Finding $finding): Response|bool

View File

@ -28,7 +28,6 @@ class RoleCapabilityMap
Capabilities::TENANT_FINDINGS_RESOLVE,
Capabilities::TENANT_FINDINGS_CLOSE,
Capabilities::TENANT_FINDINGS_RISK_ACCEPT,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
Capabilities::FINDING_EXCEPTION_VIEW,
Capabilities::FINDING_EXCEPTION_MANAGE,
Capabilities::TENANT_VERIFICATION_ACKNOWLEDGE,
@ -74,7 +73,6 @@ class RoleCapabilityMap
Capabilities::TENANT_FINDINGS_RESOLVE,
Capabilities::TENANT_FINDINGS_CLOSE,
Capabilities::TENANT_FINDINGS_RISK_ACCEPT,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
Capabilities::FINDING_EXCEPTION_VIEW,
Capabilities::FINDING_EXCEPTION_MANAGE,
Capabilities::TENANT_VERIFICATION_ACKNOWLEDGE,
@ -112,7 +110,6 @@ class RoleCapabilityMap
Capabilities::TENANT_INVENTORY_SYNC_RUN,
Capabilities::TENANT_FINDINGS_VIEW,
Capabilities::TENANT_FINDINGS_TRIAGE,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
Capabilities::FINDING_EXCEPTION_VIEW,
Capabilities::TENANT_MEMBERSHIP_VIEW,

View File

@ -46,17 +46,13 @@ public static function meaningfulActivityActionValues(): array
public function triage(Finding $finding, Tenant $tenant, User $actor): Finding
{
$this->authorize($finding, $tenant, $actor, [
Capabilities::TENANT_FINDINGS_TRIAGE,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
]);
$this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_TRIAGE]);
$currentStatus = (string) $finding->status;
if (! in_array($currentStatus, [
Finding::STATUS_NEW,
Finding::STATUS_REOPENED,
Finding::STATUS_ACKNOWLEDGED,
], true)) {
throw new InvalidArgumentException('Finding cannot be triaged from the current status.');
}
@ -82,12 +78,9 @@ public function triage(Finding $finding, Tenant $tenant, User $actor): Finding
public function startProgress(Finding $finding, Tenant $tenant, User $actor): Finding
{
$this->authorize($finding, $tenant, $actor, [
Capabilities::TENANT_FINDINGS_TRIAGE,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
]);
$this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_TRIAGE]);
if (! in_array((string) $finding->status, [Finding::STATUS_TRIAGED, Finding::STATUS_ACKNOWLEDGED], true)) {
if ((string) $finding->status !== Finding::STATUS_TRIAGED) {
throw new InvalidArgumentException('Finding cannot be moved to in-progress from the current status.');
}
@ -369,10 +362,7 @@ private function riskAcceptWithoutAuthorization(Finding $finding, Tenant $tenant
public function reopen(Finding $finding, Tenant $tenant, User $actor, string $reason): Finding
{
$this->authorize($finding, $tenant, $actor, [
Capabilities::TENANT_FINDINGS_TRIAGE,
Capabilities::TENANT_FINDINGS_ACKNOWLEDGE,
]);
$this->authorize($finding, $tenant, $actor, [Capabilities::TENANT_FINDINGS_TRIAGE]);
if (! in_array((string) $finding->status, Finding::terminalStatuses(), true)) {
throw new InvalidArgumentException('Only terminal findings can be reopened.');

View File

@ -128,7 +128,7 @@ private function openRisksSection(?EvidenceSnapshotItem $findingsItem): array
{
$summary = $this->summary($findingsItem);
$entries = collect(Arr::wrap($summary['entries'] ?? []))
->filter(static fn (mixed $entry): bool => is_array($entry) && in_array((string) ($entry['status'] ?? ''), ['open', 'in_progress', 'acknowledged'], true))
->filter(static fn (mixed $entry): bool => is_array($entry) && in_array((string) ($entry['status'] ?? ''), ['open', 'new', 'triaged', 'in_progress', 'reopened'], true))
->sortByDesc(static fn (array $entry): int => match ((string) ($entry['severity'] ?? 'low')) {
'critical' => 4,
'high' => 3,

View File

@ -91,8 +91,6 @@ class Capabilities
public const TENANT_FINDINGS_RISK_ACCEPT = 'tenant_findings.risk_accept';
public const TENANT_FINDINGS_ACKNOWLEDGE = 'tenant_findings.acknowledge';
public const FINDING_EXCEPTION_VIEW = 'finding_exception.view';
public const FINDING_EXCEPTION_MANAGE = 'finding_exception.manage';

View File

@ -796,7 +796,6 @@ private static function findingAttentionCounts(Tenant $tenant): array
$activeNonNewFindingsCount = Finding::query()
->where('tenant_id', $tenantId)
->whereIn('status', [
Finding::STATUS_ACKNOWLEDGED,
Finding::STATUS_TRIAGED,
Finding::STATUS_IN_PROGRESS,
Finding::STATUS_REOPENED,

View File

@ -99,7 +99,7 @@ private static function resolveTenantWorkspaceId(Model $model, int $tenantId): i
$tenant = $model->relationLoaded('tenant') ? $model->getRelation('tenant') : null;
if (! $tenant instanceof Tenant || (int) $tenant->getKey() !== $tenantId) {
$tenant = Tenant::query()->find($tenantId);
$tenant = Tenant::query()->withTrashed()->find($tenantId);
}
if (! $tenant instanceof Tenant) {

View File

@ -103,9 +103,9 @@ public static function baselineProfileStatuses(): array
/**
* @return array<string, string>
*/
public static function findingStatuses(bool $includeLegacyAcknowledged = true): array
public static function findingStatuses(): array
{
$options = self::badgeOptions(BadgeDomain::FindingStatus, [
return self::badgeOptions(BadgeDomain::FindingStatus, [
Finding::STATUS_NEW,
Finding::STATUS_TRIAGED,
Finding::STATUS_IN_PROGRESS,
@ -114,21 +114,6 @@ public static function findingStatuses(bool $includeLegacyAcknowledged = true):
Finding::STATUS_CLOSED,
Finding::STATUS_RISK_ACCEPTED,
]);
if (! $includeLegacyAcknowledged) {
return $options;
}
return [
Finding::STATUS_NEW => $options[Finding::STATUS_NEW],
Finding::STATUS_TRIAGED => $options[Finding::STATUS_TRIAGED],
Finding::STATUS_ACKNOWLEDGED => self::legacyFindingAcknowledgedLabel(),
Finding::STATUS_IN_PROGRESS => $options[Finding::STATUS_IN_PROGRESS],
Finding::STATUS_REOPENED => $options[Finding::STATUS_REOPENED],
Finding::STATUS_RESOLVED => $options[Finding::STATUS_RESOLVED],
Finding::STATUS_CLOSED => $options[Finding::STATUS_CLOSED],
Finding::STATUS_RISK_ACCEPTED => $options[Finding::STATUS_RISK_ACCEPTED],
];
}
/**
@ -312,11 +297,6 @@ private static function badgeOptions(BadgeDomain $domain, array $values): array
->all();
}
private static function legacyFindingAcknowledgedLabel(): string
{
return BadgeCatalog::spec(BadgeDomain::FindingStatus, Finding::STATUS_ACKNOWLEDGED)->label.' (legacy acknowledged)';
}
private static function platformLabel(string $platform): string
{
return match (Str::of($platform)

View File

@ -289,27 +289,36 @@ private static function operationAliases(): array
return [
new OperationTypeAlias('policy.sync', 'policy.sync', 'canonical', true),
new OperationTypeAlias('policy.sync_one', 'policy.sync', 'legacy_alias', false, 'Legacy single-policy sync values resolve to the canonical policy.sync operation.', 'Prefer policy.sync on platform-owned read paths.'),
new OperationTypeAlias('policy.snapshot', 'policy.snapshot', 'canonical', true),
new OperationTypeAlias('policy.capture_snapshot', 'policy.snapshot', 'canonical', true),
new OperationTypeAlias('policy.delete', 'policy.delete', 'canonical', true),
new OperationTypeAlias('policy.restore', 'policy.restore', 'canonical', true),
new OperationTypeAlias('policy.unignore', 'policy.restore', 'legacy_alias', false, 'Legacy policy.unignore values resolve to policy.restore for operator-facing wording.', 'Prefer policy.restore on new platform-owned read models.'),
new OperationTypeAlias('policy.export', 'policy.export', 'canonical', true),
new OperationTypeAlias('provider.connection.check', 'provider.connection.check', 'canonical', true),
new OperationTypeAlias('inventory.sync', 'inventory.sync', 'canonical', true),
new OperationTypeAlias('inventory_sync', 'inventory.sync', 'legacy_alias', false, 'Legacy inventory_sync storage values resolve to the canonical inventory.sync operation.', 'Preserve stored values during rollout while showing inventory.sync semantics on read paths.'),
new OperationTypeAlias('provider.inventory.sync', 'inventory.sync', 'legacy_alias', false, 'Provider-prefixed historical inventory sync values share the same operator meaning as inventory sync.', 'Avoid emitting provider.inventory.sync on new platform-owned surfaces.'),
new OperationTypeAlias('compliance.snapshot', 'compliance.snapshot', 'canonical', true),
new OperationTypeAlias('provider.compliance.snapshot', 'compliance.snapshot', 'legacy_alias', false, 'Provider-prefixed compliance snapshot values resolve to the canonical compliance.snapshot operation.', 'Avoid emitting provider.compliance.snapshot on new platform-owned surfaces.'),
new OperationTypeAlias('directory.groups.sync', 'directory.groups.sync', 'canonical', true),
new OperationTypeAlias('entra_group_sync', 'directory.groups.sync', 'legacy_alias', false, 'Historical entra_group_sync values resolve to directory.groups.sync.', 'Prefer directory.groups.sync on new platform-owned read models.'),
new OperationTypeAlias('backup_set.update', 'backup_set.update', 'canonical', true),
new OperationTypeAlias('backup_set.archive', 'backup_set.archive', 'canonical', true),
new OperationTypeAlias('backup_set.delete', 'backup_set.archive', 'canonical', true),
new OperationTypeAlias('backup_set.restore', 'backup_set.restore', 'canonical', true),
new OperationTypeAlias('backup_set.force_delete', 'backup_set.delete', 'legacy_alias', false, 'Force-delete wording is normalized to the canonical delete label.', 'Use backup_set.delete for new platform-owned summaries.'),
new OperationTypeAlias('backup.schedule.execute', 'backup.schedule.execute', 'canonical', true),
new OperationTypeAlias('backup_schedule_run', 'backup.schedule.execute', 'legacy_alias', false, 'Historical backup_schedule_run values resolve to backup.schedule.execute.', 'Prefer backup.schedule.execute on canonical read paths.'),
new OperationTypeAlias('backup.schedule.retention', 'backup.schedule.retention', 'canonical', true),
new OperationTypeAlias('backup_schedule_retention', 'backup.schedule.retention', 'legacy_alias', false, 'Legacy backup schedule retention values resolve to backup.schedule.retention.', 'Prefer dotted canonical backup schedule naming on new read paths.'),
new OperationTypeAlias('backup.schedule.purge', 'backup.schedule.purge', 'canonical', true),
new OperationTypeAlias('backup_schedule_purge', 'backup.schedule.purge', 'legacy_alias', false, 'Legacy backup schedule purge values resolve to backup.schedule.purge.', 'Prefer dotted canonical backup schedule naming on new read paths.'),
new OperationTypeAlias('restore.execute', 'restore.execute', 'canonical', true),
new OperationTypeAlias('assignments.fetch', 'assignments.fetch', 'canonical', true),
new OperationTypeAlias('assignments.restore', 'assignments.restore', 'canonical', true),
new OperationTypeAlias('ops.reconcile_adapter_runs', 'ops.reconcile_adapter_runs', 'canonical', true),
new OperationTypeAlias('directory.role_definitions.sync', 'directory.role_definitions.sync', 'canonical', true),
new OperationTypeAlias('directory_role_definitions.sync', 'directory.role_definitions.sync', 'legacy_alias', false, 'Legacy directory_role_definitions.sync values resolve to directory.role_definitions.sync.', 'Prefer dotted role-definition naming on new read paths.'),
new OperationTypeAlias('restore_run.delete', 'restore_run.delete', 'canonical', true),
new OperationTypeAlias('restore_run.restore', 'restore_run.restore', 'canonical', true),
@ -324,6 +333,7 @@ private static function operationAliases(): array
new OperationTypeAlias('baseline.compare', 'baseline.compare', 'canonical', true),
new OperationTypeAlias('baseline_capture', 'baseline.capture', 'legacy_alias', false, 'Historical baseline_capture values resolve to baseline.capture.', 'Prefer baseline.capture on canonical read paths.'),
new OperationTypeAlias('baseline_compare', 'baseline.compare', 'legacy_alias', false, 'Historical baseline_compare values resolve to baseline.compare.', 'Prefer baseline.compare on canonical read paths.'),
new OperationTypeAlias('permission.posture.check', 'permission.posture.check', 'canonical', true),
new OperationTypeAlias('permission_posture_check', 'permission.posture.check', 'legacy_alias', false, 'Historical permission_posture_check values resolve to permission.posture.check.', 'Prefer dotted permission posture naming on new read paths.'),
new OperationTypeAlias('entra.admin_roles.scan', 'entra.admin_roles.scan', 'canonical', true),
new OperationTypeAlias('tenant.review_pack.generate', 'tenant.review_pack.generate', 'canonical', true),

View File

@ -73,18 +73,6 @@ public function permissionPosture(): static
]);
}
/**
* State for legacy acknowledged findings.
*/
public function acknowledged(): static
{
return $this->state(fn (array $attributes): array => [
'status' => Finding::STATUS_ACKNOWLEDGED,
'acknowledged_at' => now(),
'acknowledged_by_user_id' => null,
]);
}
/**
* State for triaged findings.
*/

View File

@ -1,7 +1,10 @@
@php
use App\Support\Verification\VerificationLinkBehavior;
$help = is_array($help ?? null) ? $help : [];
$links = is_array($help['docs_links'] ?? null) ? $help['docs_links'] : [];
$steps = is_array($help['troubleshooting_steps'] ?? null) ? $help['troubleshooting_steps'] : [];
$linkBehavior = app(VerificationLinkBehavior::class);
$headline = is_string($help['headline'] ?? null) && trim((string) ($help['headline'] ?? '')) !== ''
? (string) ($help['headline'])
: 'Contextual help';
@ -57,9 +60,16 @@
<div class="flex flex-wrap gap-2">
@foreach ($links as $link)
@php
$linkLabel = is_string($link['label'] ?? null) && trim((string) ($link['label'] ?? '')) !== ''
? (string) $link['label']
: 'Open';
$linkUrl = is_string($link['url'] ?? null) && trim((string) ($link['url'] ?? '')) !== ''
? (string) $link['url']
: null;
$behavior = $linkUrl !== null
? $linkBehavior->describe($linkLabel, $linkUrl)
: null;
$testId = 'contextual-help-link-'.\Illuminate\Support\Str::slug($linkLabel);
@endphp
@if ($linkUrl)
@ -68,8 +78,11 @@
:href="$linkUrl"
size="sm"
color="primary"
:target="(bool) ($behavior['opens_in_new_tab'] ?? false) ? '_blank' : null"
:rel="(bool) ($behavior['opens_in_new_tab'] ?? false) ? 'noopener noreferrer' : null"
:data-testid="$testId"
>
{{ (string) ($link['label'] ?? 'Open') }}
{{ $linkLabel }}
</x-filament::button>
@endif
@endforeach

View File

@ -61,18 +61,6 @@
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$visibleSelectValue = <<<'JS'
(() => {
const select = [...document.querySelectorAll('select')].find((element) => {
const style = window.getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
});
return select?.value ?? null;
})()
JS;
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
$page
@ -87,8 +75,8 @@
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->assertSee('Status: Not started')
->click('Provider connection')
->assertScript($visibleSelectValue, (string) $connection->getKey())
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection')
->click('Create new connection')
->check('internal:label="Dedicated override"s')
->fill('[type="password"]', 'browser-only-secret')
@ -97,8 +85,8 @@
->waitForText('Status: Not started')
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Verify access')
->click('Provider connection')
->assertScript($visibleSelectValue, (string) $connection->getKey())
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection')
->click('Create new connection')
->check('internal:label="Dedicated override"s')
->assertValue('[type="password"]', '');

View File

@ -86,18 +86,6 @@
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $workspace->getKey());
$visibleSelectValue = <<<'JS'
(() => {
const select = [...document.querySelectorAll('select')].find((element) => {
const style = window.getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
});
return select?.value ?? null;
})()
JS;
$page = visit(route('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()]));
$page
@ -113,8 +101,8 @@
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertSee('Status: Needs attention')
->assertSee('Start verification')
->click('Provider connection')
->assertScript($visibleSelectValue, (string) $selectedConnection->getKey());
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection');
});
it('preserves bootstrap revisit state and blocked activation guards after refresh', function (): void {
@ -328,32 +316,14 @@
->assertNoJavaScriptErrors()
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->wait(1)
->assertScript("document.querySelector('[data-testid=\"verification-assist-trigger\"]') !== null", true)
->click('[data-testid="verification-assist-trigger"]')
->assertScript("document.querySelector('[data-testid=\"verification-assist-root\"]') !== null", true)
->assertAttribute('[data-testid="verification-assist-full-page"]', 'target', '_blank');
$page->script(<<<'JS'
Object.defineProperty(navigator, 'clipboard', {
configurable: true,
value: {
writeText: async () => Promise.resolve(),
},
});
document.querySelector('[data-testid="verification-assist-copy-application"]')?.click();
JS);
$page
->waitForText('Copied')
->assertAttribute('[data-testid="verification-assist-full-page"]', 'rel', 'noopener noreferrer')
->click('[data-testid="verification-assist-full-page"]')
->assertScript("document.querySelector('[data-testid=\"contextual-help-link-open-required-permissions\"]') !== null", true)
->assertAttribute('[data-testid="contextual-help-link-open-required-permissions"]', 'target', '_blank')
->assertAttribute('[data-testid="contextual-help-link-open-required-permissions"]', 'rel', 'noopener noreferrer')
->click('[data-testid="contextual-help-link-open-required-permissions"]')
->wait(1)
->assertRoute('admin.onboarding.draft', ['onboardingDraft' => (int) $draft->getKey()])
->assertScript("document.querySelector('[data-testid=\"verification-assist-root\"]') !== null", true)
->click('Close')
->click('Provider connection')
->assertSee('Select an existing connection or create a new one.');
->click('Select an existing connection or create a new one.')
->assertSee('Edit selected connection');
});
it('opens the permissions assist from report remediation steps without leaving onboarding', function (): void {

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
use App\Services\Auth\RoleCapabilityMap;
use App\Support\Auth\Capabilities;
use App\Support\TenantRole;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
uses(RefreshDatabase::class);
it('removes the acknowledged findings capability alias from shared RBAC truth', function (): void {
expect(Capabilities::isKnown('tenant_findings.acknowledge'))->toBeFalse();
expect(RoleCapabilityMap::rolesWithCapability('tenant_findings.acknowledge'))->toBe([]);
});
it('keeps the canonical findings triage capability available to operators', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'operator');
expect(RoleCapabilityMap::hasCapability(TenantRole::Operator, Capabilities::TENANT_FINDINGS_TRIAGE))->toBeTrue();
expect(Gate::forUser($user)->allows(Capabilities::TENANT_FINDINGS_TRIAGE, $tenant))->toBeTrue();
});

View File

@ -9,6 +9,7 @@
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\OperationRunType;
it('writes audit events for baseline capture start and completion with scope + gap summary', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -35,7 +36,7 @@
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -58,7 +58,7 @@
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -12,6 +12,7 @@
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\OperationRunType;
it('Baseline capture stores content fidelity hash when PolicyVersion evidence exists', function () {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -73,7 +74,7 @@
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -18,6 +18,7 @@
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\Baselines\PolicyVersionCapturePurpose;
use App\Support\OperationRunType;
it('Baseline capture (full content) captures evidence on demand when missing', function () {
config()->set('tenantpilot.baselines.full_content_capture.enabled', true);
@ -119,7 +120,7 @@ public function capture(
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -64,7 +64,7 @@
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -14,6 +14,7 @@
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\OperationRunType;
it('captures intune role definitions with identity metadata and excludes role assignments from the baseline snapshot', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -104,7 +105,7 @@
$operationRuns = app(OperationRunService::class);
$run = $operationRuns->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -17,6 +17,7 @@
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\Baselines\BaselineSnapshotLifecycleState;
use App\Support\Baselines\BaselineSubjectKey;
use App\Support\OperationRunType;
use Illuminate\Support\Facades\Queue;
function createBaselineCaptureInventoryBasis(
@ -65,7 +66,7 @@ function runBaselineCaptureJob(
/** @var OperationRun $run */
$run = $result['run'];
expect($run->type)->toBe('baseline_capture');
expect($run->type)->toBe(OperationRunType::BaselineCapture->value);
expect($run->status)->toBe('queued');
expect($run->tenant_id)->toBe((int) $tenant->getKey());
@ -104,7 +105,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe(BaselineReasonCodes::CAPTURE_INVENTORY_MISSING);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture when the latest inventory sync was blocked', function () {
@ -135,7 +136,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe(BaselineReasonCodes::CAPTURE_INVENTORY_BLOCKED);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture when the latest inventory sync failed without falling back to an older success', function () {
@ -166,7 +167,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe(BaselineReasonCodes::CAPTURE_INVENTORY_FAILED);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture when the latest inventory coverage is unusable for the baseline scope', function () {
@ -189,7 +190,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe(BaselineReasonCodes::CAPTURE_UNUSABLE_COVERAGE);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture for a draft profile with reason code', function () {
@ -209,7 +210,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe('baseline.capture.profile_not_active');
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture for an archived profile with reason code', function () {
@ -228,7 +229,7 @@ function runBaselineCaptureJob(
expect($result['reason_code'])->toBe('baseline.capture.profile_not_active');
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('rejects capture for a tenant from a different workspace', function () {
@ -274,7 +275,7 @@ function runBaselineCaptureJob(
expect($result2['ok'])->toBeTrue();
expect($result1['run']->getKey())->toBe($result2['run']->getKey());
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(1);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(1);
});
// --- Snapshot dedupe + capture job execution ---
@ -321,7 +322,7 @@ function runBaselineCaptureJob(
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
@ -476,7 +477,7 @@ function runBaselineCaptureJob(
$run1 = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
@ -499,7 +500,7 @@ function runBaselineCaptureJob(
'tenant_id' => (int) $tenant->getKey(),
'user_id' => (int) $user->getKey(),
'initiator_name' => $user->name,
'type' => 'baseline_capture',
'type' => OperationRunType::BaselineCapture->value,
'status' => 'queued',
'outcome' => 'pending',
'run_identity_hash' => hash('sha256', 'second-run-'.now()->timestamp),
@ -586,7 +587,7 @@ function runBaselineCaptureJob(
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
@ -662,7 +663,7 @@ function runBaselineCaptureJob(
$opService = app(OperationRunService::class);
$run = $opService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -15,6 +15,7 @@
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
@ -69,14 +70,14 @@
$activeRuns = OperationRun::query()
->where('workspace_id', (int) $fixture['workspace']->getKey())
->where('type', 'baseline_compare')
->where('type', OperationRunType::BaselineCompare->value)
->get();
expect($activeRuns)->toHaveCount(2)
->and($activeRuns->every(static fn (OperationRun $run): bool => $run->tenant_id !== null))->toBeTrue()
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->status === OperationRunStatus::Queued->value))->toBeTrue()
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->outcome === OperationRunOutcome::Pending->value))->toBeTrue()
->and(OperationRun::query()->whereNull('tenant_id')->where('type', 'baseline_compare')->count())->toBe(0);
->and(OperationRun::query()->whereNull('tenant_id')->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});
it('runs compare assigned tenants from the matrix page and keeps feedback on tenant-owned runs', function (): void {
@ -97,7 +98,7 @@
expect(OperationRun::query()
->where('workspace_id', (int) $fixture['workspace']->getKey())
->where('type', 'baseline_compare')
->where('type', OperationRunType::BaselineCompare->value)
->whereNull('tenant_id')
->count())->toBe(0);
});

View File

@ -3,6 +3,7 @@
use App\Jobs\EntraGroupSyncJob;
use App\Services\Directory\EntraGroupSyncService;
use App\Services\Providers\ProviderOperationStartResult;
use App\Support\OperationRunType;
use Illuminate\Support\Facades\Queue;
it('starts a manual group sync by creating a run and dispatching a job', function () {
@ -21,7 +22,7 @@
expect($run)
->and($run->tenant_id)->toBe($tenant->getKey())
->and($run->user_id)->toBe($user->getKey())
->and($run->type)->toBe('entra_group_sync')
->and($run->type)->toBe(OperationRunType::DirectoryGroupsSync->value)
->and($run->status)->toBe('queued')
->and($run->context['selection_key'] ?? null)->toBe('groups-v1:all')
->and($run->context['provider_connection_id'] ?? null)->toBeInt();

View File

@ -7,6 +7,7 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\OperationRunType;
it('sync job upserts groups and updates run counters', function () {
@ -54,7 +55,7 @@
$opService = app(OperationRunService::class);
$opRun = $opService->ensureRun(
tenant: $tenant,
type: 'entra_group_sync',
type: OperationRunType::DirectoryGroupsSync->value,
inputs: ['selection_key' => 'groups-v1:all'],
initiator: $user,
);

View File

@ -7,6 +7,7 @@
use App\Services\Graph\GraphResponse;
use App\Services\Intune\AuditLogger;
use App\Services\OperationRunService;
use App\Support\OperationRunType;
use Illuminate\Support\Facades\Config;
it('purges cached groups older than the retention window', function () {
@ -34,7 +35,7 @@
$opService = app(OperationRunService::class);
$opRun = $opService->ensureRun(
tenant: $tenant,
type: 'entra_group_sync',
type: OperationRunType::DirectoryGroupsSync->value,
inputs: ['selection_key' => 'groups-v1:all'],
initiator: $user,
);

View File

@ -444,7 +444,7 @@ function makeGenerator(): EntraAdminRolesFindingGenerator
->and($finding->subject_external_id)->toBe('user-1:def-ga');
});
it('auto-resolve applies to acknowledged findings too', function (): void {
it('auto-resolve applies to triaged findings too', function (): void {
[$user, $tenant] = createMinimalUserWithTenant();
$generator = makeGenerator();
@ -456,20 +456,19 @@ function makeGenerator(): EntraAdminRolesFindingGenerator
);
$generator->generate($tenant, $payload);
// Acknowledge the finding
// Triage the finding
$finding = Finding::query()
->where('tenant_id', $tenant->getKey())
->where('subject_external_id', 'user-1:def-ga')
->first();
$finding->forceFill([
'status' => Finding::STATUS_ACKNOWLEDGED,
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
'status' => Finding::STATUS_TRIAGED,
'triaged_at' => now(),
])->save();
expect($finding->fresh()->status)->toBe(Finding::STATUS_ACKNOWLEDGED);
expect($finding->fresh()->status)->toBe(Finding::STATUS_TRIAGED);
// Scan 2: remove → should auto-resolve even though acknowledged
// Scan 2: remove -> should auto-resolve even though triaged
$payload2 = buildPayload([gaRoleDef()], []);
$result = $generator->generate($tenant, $payload2);

View File

@ -115,7 +115,7 @@
$run = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'baseline_compare')
->where('type', OperationRunType::BaselineCompare->value)
->latest('id')
->first();
@ -192,7 +192,7 @@
->assertStatus(200);
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
expect(OperationRun::query()->where('type', 'baseline_compare')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});
it('shows mixed-strategy compare rejection truth on the tenant landing surface', function (): void {
@ -250,7 +250,7 @@
->assertStatus(200);
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
expect(OperationRun::query()->where('type', 'baseline_compare')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});
it('can refresh stats without calling mount directly', function (): void {

View File

@ -9,6 +9,7 @@
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\OperationRunType;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@ -121,7 +122,7 @@ function seedCaptureProfileForTenant(
$run = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'baseline_capture')
->where('type', OperationRunType::BaselineCapture->value)
->latest('id')
->first();
@ -151,7 +152,7 @@ function seedCaptureProfileForTenant(
->assertStatus(200);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('does not start full-content capture when rollout is disabled', function (): void {
@ -174,7 +175,7 @@ function seedCaptureProfileForTenant(
->assertStatus(200);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});
it('shows readiness copy without exposing raw canonical scope json on the capture start surface', function (): void {
@ -228,5 +229,5 @@ function seedCaptureProfileForTenant(
->assertStatus(200);
Queue::assertNotPushed(CaptureBaselineSnapshotJob::class);
expect(OperationRun::query()->where('type', 'baseline_capture')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCapture->value)->count())->toBe(0);
});

View File

@ -14,6 +14,7 @@
use App\Support\Baselines\Compare\CompareStrategyRegistry;
use App\Support\Baselines\Compare\IntuneCompareStrategy;
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
use App\Support\OperationRunType;
use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action;
use Filament\Actions\ActionGroup;
@ -92,7 +93,7 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
$run = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'baseline_compare')
->where('type', OperationRunType::BaselineCompare->value)
->latest('id')
->first();
@ -120,7 +121,7 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
->assertStatus(200);
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
expect(OperationRun::query()->where('type', 'baseline_compare')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});
it('shows mixed-strategy compare rejection truth on the workspace start surface', function (): void {
@ -167,7 +168,7 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
->assertStatus(200);
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
expect(OperationRun::query()->where('type', 'baseline_compare')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});
it('moves compare-matrix navigation into related context while keeping compare-assigned-tenants secondary', function (): void {
@ -275,5 +276,5 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
->assertStatus(200);
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
expect(OperationRun::query()->where('type', 'baseline_compare')->count())->toBe(0);
expect(OperationRun::query()->where('type', OperationRunType::BaselineCompare->value)->count())->toBe(0);
});

View File

@ -34,7 +34,6 @@ protected function makeFindingForWorkflow(Tenant $tenant, string $status = Findi
$factory = Finding::factory()->for($tenant);
$factory = match ($status) {
Finding::STATUS_ACKNOWLEDGED => $factory->acknowledged(),
Finding::STATUS_TRIAGED => $factory->triaged(),
Finding::STATUS_IN_PROGRESS => $factory->inProgress(),
Finding::STATUS_REOPENED => $factory->reopened(),

View File

@ -22,9 +22,8 @@
->toContain(AuditActionId::FindingReopened->value);
});
it('keeps only legacy compatibility lifecycle helpers on the model', function (): void {
expect(method_exists(Finding::class, 'acknowledge'))->toBeTrue()
->and(method_exists(Finding::class, 'resolve'))->toBeTrue()
it('keeps only the surviving model lifecycle helpers', function (): void {
expect(method_exists(Finding::class, 'resolve'))->toBeTrue()
->and(method_exists(Finding::class, 'reopen'))->toBeTrue()
->and(method_exists(Finding::class, 'triage'))->toBeFalse()
->and(method_exists(Finding::class, 'startProgress'))->toBeFalse()

View File

@ -101,8 +101,10 @@ function makeIntakeFinding(Tenant $tenant, array $attributes = []): Finding
'assignee_user_id' => (int) $otherAssignee->getKey(),
]);
$acknowledged = Finding::factory()->for($tenantA)->acknowledged()->create([
$acknowledged = Finding::factory()->for($tenantA)->create([
'workspace_id' => (int) $tenantA->workspace_id,
'status' => 'acknowledged',
'acknowledged_at' => now(),
'assignee_user_id' => null,
'subject_external_id' => 'acknowledged',
]);

View File

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
use App\Models\Finding;
use App\Services\Findings\FindingWorkflowService;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('rejects triage from the removed acknowledged status', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = Finding::factory()->for($tenant)->create([
'status' => 'acknowledged',
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
]);
expect(fn () => app(FindingWorkflowService::class)->triage($finding, $tenant, $user))
->toThrow(\InvalidArgumentException::class, 'Finding cannot be triaged from the current status.');
});
it('rejects start progress from the removed acknowledged status', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = Finding::factory()->for($tenant)->create([
'status' => 'acknowledged',
'triaged_at' => now()->subMinute(),
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
]);
expect(fn () => app(FindingWorkflowService::class)->startProgress($finding, $tenant, $user))
->toThrow(\InvalidArgumentException::class, 'Finding cannot be moved to in-progress from the current status.');
});

View File

@ -59,15 +59,18 @@
]);
});
it('supports legacy model helper compatibility for acknowledge', function (): void {
it('keeps stale acknowledged metadata as passive data only', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = Finding::factory()->for($tenant)->permissionPosture()->create();
$finding = Finding::factory()->for($tenant)->permissionPosture()->create([
'status' => 'acknowledged',
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
]);
$finding->acknowledge($user);
expect($finding->status)->toBe(Finding::STATUS_ACKNOWLEDGED)
expect($finding->status)->toBe('acknowledged')
->and($finding->acknowledged_at)->not->toBeNull()
->and($finding->acknowledged_by_user_id)->toBe($user->getKey());
->and($finding->acknowledged_by_user_id)->toBe($user->getKey())
->and($finding->hasOpenStatus())->toBeFalse();
});
it('exposes v2 open and terminal status helpers', function (): void {
@ -84,31 +87,26 @@
Finding::STATUS_RISK_ACCEPTED,
]);
expect(Finding::openStatusesForQuery())->toContain(Finding::STATUS_ACKNOWLEDGED);
expect(Finding::openStatusesForQuery())->toBe(Finding::openStatuses());
});
it('maps legacy acknowledged status to triaged in v2 helpers', function (): void {
expect(Finding::canonicalizeStatus(Finding::STATUS_ACKNOWLEDGED))
->toBe(Finding::STATUS_TRIAGED);
it('does not treat acknowledged as canonical in v2 helpers', function (): void {
expect(Finding::canonicalizeStatus('acknowledged'))->toBe('acknowledged');
expect(Finding::isOpenStatus(Finding::STATUS_ACKNOWLEDGED))->toBeTrue();
expect(Finding::isTerminalStatus(Finding::STATUS_ACKNOWLEDGED))->toBeFalse();
expect(Finding::isOpenStatus('acknowledged'))->toBeFalse();
expect(Finding::isTerminalStatus('acknowledged'))->toBeFalse();
});
it('preserves acknowledged metadata when resolving an acknowledged finding', function (): void {
it('rejects resolving a stale acknowledged finding', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$finding = Finding::factory()->for($tenant)->permissionPosture()->acknowledged()->create([
$finding = Finding::factory()->for($tenant)->permissionPosture()->create([
'status' => 'acknowledged',
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $user->getKey(),
]);
expect($finding->status)->toBe(Finding::STATUS_ACKNOWLEDGED);
$finding = app(FindingWorkflowService::class)->resolve($finding, $tenant, $user, Finding::RESOLVE_REASON_REMEDIATED);
expect($finding->status)->toBe(Finding::STATUS_RESOLVED)
->and($finding->acknowledged_at)->not->toBeNull()
->and($finding->acknowledged_by_user_id)->toBe($user->getKey())
->and($finding->resolved_at)->not->toBeNull();
expect(fn () => app(FindingWorkflowService::class)->resolve($finding, $tenant, $user, Finding::RESOLVE_REASON_REMEDIATED))
->toThrow(\InvalidArgumentException::class, 'Only open findings can be resolved.');
});
it('has STATUS_RESOLVED constant', function (): void {

View File

@ -14,6 +14,7 @@
use App\Support\Audit\AuditOutcome;
use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\BaselineReasonCodes;
use App\Support\OperationRunType;
it('derives summary-first audit semantics for baseline capture workflow events', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
@ -36,7 +37,7 @@
$operationRunService = app(OperationRunService::class);
$run = $operationRunService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),
@ -97,7 +98,7 @@
$operationRunService = app(OperationRunService::class);
$run = $operationRunService->ensureRunWithIdentity(
tenant: $tenant,
type: 'baseline_capture',
type: OperationRunType::BaselineCapture->value,
identityInputs: ['baseline_profile_id' => (int) $profile->getKey()],
context: [
'baseline_profile_id' => (int) $profile->getKey(),

View File

@ -26,7 +26,7 @@
->get('/admin/operations')
->assertOk()
->assertSee($workspaceName ?? 'Select workspace')
->assertSee('Search tenants…')
->assertSee(__('localization.shell.search_tenants'))
->assertSee('Switch workspace')
->assertSee('admin/select-tenant')
->assertSee('Clear tenant scope')
@ -66,7 +66,7 @@
->get('/admin/workspaces')
->assertOk()
->assertSee('Choose a workspace first.')
->assertDontSee('Search tenants…');
->assertDontSee(__('localization.shell.search_tenants'));
});
it('keeps tenant-scoped pages selector read-only while exposing the clear tenant scope action', function (): void {

View File

@ -93,7 +93,7 @@
'tenant_id' => (int) $tenant->getKey(),
'tableFilters' => [
'type' => [
'value' => 'inventory_sync',
'value' => 'inventory.sync',
],
],
]));

View File

@ -6,6 +6,7 @@
use App\Models\BackupSchedule;
use App\Models\BackupSet;
use App\Models\OperationRun;
use App\Support\OperationRunType;
it('completes backup retention runs without persisting terminal notifications for system runs', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager');
@ -62,7 +63,7 @@
$retentionRun = OperationRun::query()
->where('tenant_id', (int) $tenant->getKey())
->where('type', 'backup_schedule_retention')
->where('type', OperationRunType::BackupScheduleRetention->value)
->latest('id')
->first();

View File

@ -104,19 +104,17 @@ function errorPermission(string $key, array $features = []): array
->and($finding->resolved_reason)->toBe('permission_granted');
});
// (3) Auto-resolves acknowledged finding preserving metadata
it('auto-resolves acknowledged finding preserving acknowledged metadata', function (): void {
// (3) Auto-resolves triaged finding preserving triaged metadata
it('auto-resolves triaged finding preserving triaged metadata', function (): void {
[$user, $tenant] = createUserWithTenant();
$generator = app(PermissionPostureFindingGenerator::class);
$generator->generate($tenant, buildComparison([missingPermission('Perm.A')]));
$finding = Finding::query()->where('tenant_id', $tenant->getKey())->first();
$ackUser = User::factory()->create();
$finding->forceFill([
'status' => Finding::STATUS_ACKNOWLEDGED,
'acknowledged_at' => now(),
'acknowledged_by_user_id' => $ackUser->getKey(),
'status' => Finding::STATUS_TRIAGED,
'triaged_at' => now(),
])->save();
$result = $generator->generate($tenant, buildComparison([grantedPermission('Perm.A')], 'granted'));
@ -124,8 +122,7 @@ function errorPermission(string $key, array $features = []): array
$finding->refresh();
expect($result->findingsResolved)->toBe(1)
->and($finding->status)->toBe(Finding::STATUS_RESOLVED)
->and($finding->acknowledged_at)->not->toBeNull()
->and($finding->acknowledged_by_user_id)->toBe($ackUser->getKey());
->and($finding->triaged_at)->not->toBeNull();
});
// (4) No duplicates on idempotent run

View File

@ -11,7 +11,7 @@
expect($gate->allows(Capabilities::TENANT_VIEW, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_SYNC, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_ACKNOWLEDGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_TRIAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_MANAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_BACKUP_SCHEDULES_RUN, $tenant))->toBeTrue();

View File

@ -11,7 +11,7 @@
expect($gate->allows(Capabilities::TENANT_VIEW, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_SYNC, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_ACKNOWLEDGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_TRIAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::PROVIDER_VIEW, $tenant))->toBeTrue();

View File

@ -11,7 +11,7 @@
expect($gate->allows(Capabilities::TENANT_VIEW, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_SYNC, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_ACKNOWLEDGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_FINDINGS_TRIAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_MANAGE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_DELETE, $tenant))->toBeTrue();
expect($gate->allows(Capabilities::TENANT_MEMBERSHIP_MANAGE, $tenant))->toBeTrue();

View File

@ -16,7 +16,7 @@
expect($gate->allows(Capabilities::TENANT_SYNC, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_INVENTORY_SYNC_RUN, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_FINDINGS_ACKNOWLEDGE, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_FINDINGS_TRIAGE, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_MANAGE, $tenant))->toBeFalse();
expect($gate->allows(Capabilities::TENANT_DELETE, $tenant))->toBeFalse();

View File

@ -24,10 +24,10 @@
->and($spec->color)->toBe('warning');
});
it('still renders acknowledged status badge', function (): void {
$spec = BadgeCatalog::spec(BadgeDomain::FindingStatus, Finding::STATUS_ACKNOWLEDGED);
it('renders unknown for removed acknowledged status badges', function (): void {
$spec = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'acknowledged');
expect($spec->label)->toBe('Triaged')
expect($spec->label)->toBe('Unknown')
->and($spec->color)->toBe('gray');
});

View File

@ -6,6 +6,7 @@
use App\Services\Providers\ProviderOperationStartResult;
use App\Services\Directory\RoleDefinitionsSyncService;
use App\Support\OperationRunLinks;
use App\Support\OperationRunType;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
@ -36,7 +37,7 @@
$run = $result->run;
expect($run->type)->toBe('directory_role_definitions.sync');
expect($run->type)->toBe(OperationRunType::DirectoryRoleDefinitionsSync->value);
expect($run->context['provider_connection_id'] ?? null)->toBeInt();
$url = OperationRunLinks::tenantlessView($run);

View File

@ -2,6 +2,8 @@
declare(strict_types=1);
use App\Models\Finding;
it('passes shared canonical control references through tenant review composition', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 0, driftCount: 1);
@ -14,5 +16,30 @@
->and($review->canonicalControlReferences()[0]['control_key'])->toBe('endpoint_hardening_compliance')
->and($executiveSummary->summary_payload['canonical_control_count'])->toBe(1)
->and($executiveSummary->summary_payload['canonical_controls'][0]['control_key'])->toBe('endpoint_hardening_compliance')
->and($openRisks->summary_payload['canonical_controls'])->toBe([]);
->and($openRisks->summary_payload['canonical_controls'][0]['control_key'] ?? null)->toBe('endpoint_hardening_compliance');
});
it('excludes removed acknowledged findings from open risk highlights', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
Finding::factory()->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
'status' => 'acknowledged',
'subject_external_id' => 'legacy-acknowledged',
]);
$triagedFinding = Finding::factory()->for($tenant)->create([
'workspace_id' => (int) $tenant->workspace_id,
'status' => Finding::STATUS_TRIAGED,
'subject_external_id' => 'canonical-triaged',
]);
$snapshot = seedTenantReviewEvidence($tenant, findingCount: 0, driftCount: 0);
$review = composeTenantReviewForTest($tenant, $user, $snapshot);
$openRisks = $review->sections->firstWhere('section_key', 'open_risks');
$entries = $openRisks->render_payload['entries'] ?? [];
expect($entries)->toHaveCount(1)
->and($entries[0]['id'] ?? null)->toBe((int) $triagedFinding->getKey())
->and(collect($entries)->pluck('status')->all())->not->toContain('acknowledged');
});

View File

@ -23,7 +23,7 @@
->assertSee('Tenant Panel Entry')
->assertSee('Switch tenant')
->assertSee('Clear tenant scope')
->assertDontSee('Search tenants…')
->assertDontSee(__('localization.shell.search_tenants'))
->assertDontSee('admin/select-tenant');
});

View File

@ -33,7 +33,7 @@
expect($triaged->color)->toBe('gray');
$legacyAcknowledged = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'acknowledged');
expect($legacyAcknowledged->label)->toBe('Triaged');
expect($legacyAcknowledged->label)->toBe('Unknown');
expect($legacyAcknowledged->color)->toBe('gray');
$inProgress = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'in_progress');

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use App\Models\Finding;
it('exposes only canonical open statuses for findings', function (): void {
expect(Finding::openStatuses())->toBe([
Finding::STATUS_NEW,
Finding::STATUS_TRIAGED,
Finding::STATUS_IN_PROGRESS,
Finding::STATUS_REOPENED,
]);
expect(Finding::openStatusesForQuery())->toBe(Finding::openStatuses());
});
it('does not treat acknowledged as a canonical open or terminal status', function (): void {
expect(Finding::canonicalizeStatus('acknowledged'))->toBe('acknowledged');
expect(Finding::isOpenStatus('acknowledged'))->toBeFalse();
expect(Finding::isTerminalStatus('acknowledged'))->toBeFalse();
});

View File

@ -9,15 +9,14 @@
$types = app(OperationLifecyclePolicy::class)->coveredTypeNames();
expect($types)->toBe([
'baseline_capture',
'baseline_compare',
'inventory_sync',
'baseline.capture',
'baseline.compare',
'inventory.sync',
'policy.sync',
'policy.sync_one',
'entra_group_sync',
'directory_role_definitions.sync',
'directory.groups.sync',
'directory.role_definitions.sync',
'backup_set.update',
'backup_schedule_run',
'backup.schedule.execute',
'restore.execute',
'tenant.review_pack.generate',
'tenant.review.compose',
@ -28,19 +27,19 @@
it('requires direct failed-job bridges for lifecycle policy entries that declare them', function (): void {
$validator = app(OperationLifecyclePolicyValidator::class);
expect($validator->jobUsesDirectFailedBridge('baseline_capture'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('baseline_compare'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('inventory_sync'))->toBeTrue()
expect($validator->jobUsesDirectFailedBridge('baseline.capture'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('baseline.compare'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('inventory.sync'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('policy.sync'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('tenant.review.compose'))->toBeTrue()
->and($validator->jobUsesDirectFailedBridge('backup_schedule_run'))->toBeFalse();
->and($validator->jobUsesDirectFailedBridge('backup.schedule.execute'))->toBeFalse();
});
it('requires explicit timeout and fail-on-timeout declarations for covered jobs', function (): void {
$validator = app(OperationLifecyclePolicyValidator::class);
expect($validator->jobTimeoutSeconds('baseline_capture'))->toBe(300)
->and($validator->jobFailsOnTimeout('baseline_capture'))->toBeTrue()
expect($validator->jobTimeoutSeconds('baseline.capture'))->toBe(300)
->and($validator->jobFailsOnTimeout('baseline.capture'))->toBeTrue()
->and($validator->jobTimeoutSeconds('backup_set.update'))->toBe(240)
->and($validator->jobFailsOnTimeout('backup_set.update'))->toBeTrue()
->and($validator->jobTimeoutSeconds('restore.execute'))->toBe(420)

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
use App\Models\Finding;
use App\Support\Filament\FilterOptionCatalog;
it('exposes only canonical finding statuses in the shared filter catalog', function (): void {
expect(FilterOptionCatalog::findingStatuses())->toBe([
Finding::STATUS_NEW => 'New',
Finding::STATUS_TRIAGED => 'Triaged',
Finding::STATUS_IN_PROGRESS => 'In progress',
Finding::STATUS_REOPENED => 'Reopened',
Finding::STATUS_RESOLVED => 'Resolved',
Finding::STATUS_CLOSED => 'Closed',
Finding::STATUS_RISK_ACCEPTED => 'Risk accepted',
]);
});
it('does not offer acknowledged as a legacy findings filter option', function (): void {
expect(FilterOptionCatalog::findingStatuses())->not->toHaveKey('acknowledged');
});

View File

@ -15,6 +15,7 @@
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\PortfolioTriage\TenantTriageReviewFingerprint;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -64,6 +65,7 @@
OperationRun::factory()
->forTenant($bravoTenant)
->create([
'type' => OperationRunType::InventorySync->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'created_at' => now()->subMinutes(6),

View File

@ -53,6 +53,29 @@
]);
});
it('records a tenant-owned usage event for an archived tenant', function () {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->for($workspace)->archived()->create();
$user = User::factory()->create();
$event = app(ProductTelemetryRecorder::class)->record(
eventName: ProductUsageEventCatalog::ONBOARDING_CHECKPOINT_COMPLETED,
workspaceId: (int) $workspace->getKey(),
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
subjectType: 'tenant_onboarding_session',
subjectId: 99,
metadata: [
'checkpoint_key' => 'verify_access',
'lifecycle_state' => 'draft',
],
);
expect($event->workspace_id)->toBe((int) $workspace->getKey())
->and($event->tenant_id)->toBe((int) $tenant->getKey())
->and($event->feature_area)->toBe('onboarding');
});
it('rejects unknown event names before writing telemetry rows', function () {
$workspace = Workspace::factory()->create();
$tenant = Tenant::factory()->for($workspace)->create();

View File

@ -0,0 +1,48 @@
# Specification Quality Checklist: Remove Legacy Acknowledged Finding Status Compatibility
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-29
**Feature**: specs/254-remove-acknowledged-compat/spec.md
## Content Quality
- [x] No language/framework/API design leakage; concrete repo surfaces, status constants, capability keys, and shared helpers are named only because this cleanup removes those exact repo-visible compatibility seams.
- [x] Focused on user value and business needs
- [x] Written primarily for product and review stakeholders, with bounded repo-specific terminology only where the cleanup target would otherwise stay ambiguous
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria stay outcome-oriented, with bounded repo-specific seams named only where they are required to define the cleanup target
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature defines measurable outcomes in Success Criteria and maps them to explicit proof tasks for implementation-time validation
- [x] No unintended implementation design leakage remains beyond the explicit cleanup special-case for named repo-visible compatibility seams
## Test Governance Review
- [x] Lane fit is explicit: the package uses `fast-feedback` and `confidence`, plus bounded `heavy-governance` guard coverage so shared status-badge and filter drift cannot silently reintroduce acknowledged semantics.
- [x] No new browser or heavy-governance family is introduced; retained guard coverage stays explicit and limited to shared findings status seams.
- [x] Suite-cost outcome is net-neutral to slightly negative: acknowledged-only compatibility expectations should be consolidated or deleted rather than widening shared defaults.
## Review Outcome
- [x] Review outcome class: `acceptable-special-case`
- [x] Workflow outcome: `keep`
- [x] Review-note location is explicit: the guardrail and lane-fit notes live in `spec.md`, the checklist, and the final preparation report.
## Notes
- The spec intentionally names concrete findings status constants, capability aliases, shared catalogs, and summary builders because the product value of this slice is removing those exact compatibility seams from repo truth.
- Verification-check acknowledgement and onboarding acknowledgement remain explicit non-goals so the cleanup cannot expand into a broader terminology rewrite.
- Validation pass complete: no clarification markers remain, the slice stays LEAN-001-compliant, and tenant-owned findings continue to treat `workspace_id` plus `tenant_id` as required anchors.

View File

@ -0,0 +1,121 @@
version: 1
kind: findings-acknowledged-compat-removal
scope:
goal: remove productive acknowledged compatibility from findings workflow truth only
non_goals:
- findings lifecycle backfill runtime-surface removal
- creation-time finding invariant hardening
- broader findings lifecycle redesign
- verification acknowledgement cleanup
- onboarding acknowledgement cleanup
- restore impact acknowledgement cleanup
- migration or fallback-reader preservation
canonical_status_contract:
active_open:
- new
- triaged
- in_progress
- reopened
terminal:
- resolved
- closed
- risk_accepted
removed_active_status:
- acknowledged
shared_seams:
model_and_workflow:
owner_files:
- apps/platform/app/Models/Finding.php
- apps/platform/app/Services/Findings/FindingWorkflowService.php
- apps/platform/app/Policies/FindingPolicy.php
requirements:
- no productive findings workflow helper writes or expects acknowledged
- open-status query helpers collapse onto the canonical active-open set only
badge_and_filter_catalogs:
owner_files:
- apps/platform/app/Support/Badges/Domains/FindingStatusBadge.php
- apps/platform/app/Support/Filament/FilterOptionCatalog.php
requirements:
- no badge label exposes acknowledged or legacy acknowledged
- no findings filter offers acknowledged as a current workflow state
capabilities_and_roles:
owner_files:
- apps/platform/app/Support/Auth/Capabilities.php
- apps/platform/app/Services/Auth/RoleCapabilityMap.php
requirements:
- tenant_findings.acknowledge is removed
- surviving findings capability language stays canonical and tenant-scoped
tenant_findings_surfaces:
routes:
- /admin/t/{tenant}/findings
- /admin/t/{tenant}/findings/{record}
owner_files:
- apps/platform/app/Filament/Resources/FindingResource.php
- apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php
- apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php
requirements:
- no visible findings workflow affordance presents acknowledged as current work
findings_derived_consumers:
owner_files:
- apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php
- apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php
- apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php
- apps/platform/app/Support/Baselines/BaselineCompareStats.php
- apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php
- apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php
- apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php
- apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php
- apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php
- apps/platform/app/Services/Baselines/BaselineAutoCloseService.php
- apps/platform/app/Services/Findings/FindingAssignmentHygieneService.php
requirements:
- counts, previews, review disclosures, diagnostics, and alerts use the same canonical open-status set as findings surfaces
- no productive findings-derived consumer treats acknowledged as current work
retained_behavior:
findings_workflow_actions:
- triage
- start_progress
- assign
- resolve
- close
- reopen
- request_exception
- risk_accept
guarantees:
- existing findings lifecycle outcomes remain otherwise unchanged
- no new workflow state or replacement compatibility path is introduced
non_finding_domains:
untouched:
- verification check acknowledgement
- onboarding verification acknowledgement
- restore impact acknowledgement
legacy_data_posture:
findings_table:
- acknowledged columns may remain in schema for now without preserving active runtime semantics
migrations:
- no new migration or persisted compatibility artifact is allowed in this slice
validation_expectations:
no_new_persistence:
- no file under apps/platform/database/migrations may change
- no alias table, persisted mapping, or fallback reader may be introduced
absence_proof:
- no productive findings surface exposes acknowledged as current workflow status
- no productive findings-derived consumer exposes acknowledged as current work
- no findings capability alias remains for acknowledge semantics
regression_proof:
- canonical findings workflow actions still behave unchanged
- non-finding acknowledgement domains remain untouched
lane_classification:
required:
- fast-feedback
- confidence
- heavy-governance
excluded:
- browser

View File

@ -0,0 +1,103 @@
# Data Model — Remove Legacy Acknowledged Finding Status Compatibility
**Spec**: [spec.md](spec.md)
This feature is subtractive. It introduces no new persisted truth and no migration. The data-model impact is the removal of one legacy findings workflow branch from productive code and the reaffirmation of the canonical findings lifecycle as the only active status contract.
## Existing Canonical Entities Reused
### Finding (`findings`)
**Purpose**: Tenant-owned findings workflow truth.
**Key fields (existing)**:
- `id`
- `workspace_id`
- `tenant_id`
- `status`
- `triaged_at`
- `in_progress_at`
- `reopened_at`
- `resolved_at`
- `closed_at`
- `risk_accepted_at` via related exception state where applicable
- `first_seen_at`
- `last_seen_at`
- `times_seen`
- `sla_days`
- `due_at`
- `acknowledged_at`
- `acknowledged_by_user_id`
**Feature use**:
- Remains the single canonical workflow truth for findings.
- Continues to require both `workspace_id` and `tenant_id` as ownership anchors.
- Keeps the surviving active status contract: `new`, `triaged`, `in_progress`, `reopened`.
- Keeps the surviving terminal status contract: `resolved`, `closed`, `risk_accepted`.
- `acknowledged_at` and `acknowledged_by_user_id` may remain in schema for now, but they no longer justify an active workflow status, query branch, or UI affordance.
### FindingException (`finding_exceptions`)
**Purpose**: Existing risk-acceptance and exception truth attached to findings.
**Feature use**:
- Remains unchanged.
- Exists only for regression protection so removing `acknowledged` does not collapse or rename risk-governance semantics.
## Removed Active Workflow Contract
### LegacyAcknowledgedFindingStatus (removed, non-persisted contract)
**Previous role**:
- active status constant on `Finding`
- extra member of `openStatusesForQuery()`
- special-case filter and badge label
- capability alias and RBAC wording branch
- compatibility expectation in findings-facing tests and summary consumers
**Removal rule**:
- no productive code path writes `acknowledged` as current findings status
- no productive code path queries `acknowledged` as part of the active open-status set
- no productive findings UI or summary consumer presents `acknowledged` as current work
- no role or capability mapping preserves `tenant_findings.acknowledge`
## Derived Non-Persisted Contracts
### CanonicalFindingOpenStatusSet (derived)
**Members**:
- `new`
- `triaged`
- `in_progress`
- `reopened`
**Consumers**:
- findings resource and inbox queries
- workspace overview and governance inbox summaries
- review/report disclosure helpers that describe current open findings work
- support-diagnostic bundles that group active findings issues
- alerts, hygiene services, and findings generators that still look up active/open findings
### CanonicalFindingWorkflowPermissionSet (derived)
**Purpose**: Surviving capability vocabulary for findings workflow actions.
**Feature use**:
- remove `tenant_findings.acknowledge`
- keep surviving findings permissions and policy checks authoritative
- keep `404` versus `403` semantics unchanged for tenant-scoped findings surfaces
## Data Ownership Notes
- No new table, column, persisted alias, cache, or compatibility projection is introduced.
- No migration or historical data rewrite is planned.
- Review/report and support-diagnostic consumers remain derived over tenant-owned findings truth; they do not become separate persisted status stores.
- Verification-check acknowledgement, onboarding acknowledgement, and restore acknowledgement remain separate domains and are not remodeled here.
## Removal Invariants
- No productive code path may treat `acknowledged` as a current findings workflow status.
- No productive query helper may include `acknowledged` in the active open findings set.
- No shared badge, filter, summary, review/report disclosure, or support-diagnostic grouping may present `acknowledged` as current findings work.
- No new migration or persisted compatibility artifact may be introduced to preserve the removed branch.
- No non-finding acknowledgement domain may change as collateral damage from this cleanup.

View File

@ -0,0 +1,266 @@
# Implementation Plan: Remove Legacy Acknowledged Finding Status Compatibility
**Branch**: `254-remove-acknowledged-compat` | **Date**: 2026-04-29 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/254-remove-acknowledged-compat/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/254-remove-acknowledged-compat/spec.md`
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
## Summary
- Remove productive `acknowledged` compatibility from the findings domain by collapsing canonical status, query, badge, filter, capability, policy, and workflow seams onto the surviving findings lifecycle only.
- Keep the slice subtractive and repo-based: no new state, no migration shim, no repair tooling, no broader lifecycle redesign, and no changes to verification-check or onboarding acknowledgement domains.
- Validate the cleanup through focused workflow, summary, badge or filter, capability, and guard coverage so shared findings-derived counts and operator surfaces converge on one canonical language at the same time.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing findings workflow services, shared badge and filter catalogs, capability registry, and canonical summary builders
**Storage**: PostgreSQL existing `findings`, `finding_exceptions`, `audit_logs`, `operation_runs`, and related read models only; no new persistence or migration is planned
**Testing**: Pest unit, feature, and bounded heavy-governance guard coverage
**Validation Lanes**: fast-feedback, confidence, heavy-governance
**Target Platform**: Sail-backed Laravel web application with tenant `/admin/t/{tenant}` findings surfaces and canonical `/admin` summary or inbox surfaces
**Project Type**: web
**Performance Goals**: shared query helpers, inboxes, and summary builders keep their current bounded DB-only render profile; the slice should reduce branching and suite noise rather than add overhead
**Constraints**: LEAN-001 replacement over shims; no schema drop by default; preserve current `404` versus `403` isolation semantics; no panel or provider changes; no new assets; no widening into creation-time invariant hardening, backfill-runtime-surface work, or external support handoff
**Scale/Scope**: 1 cleanup slice touching the `Finding` model and factory, findings workflow service and policy seam, shared badge and filter catalog paths, findings resource and inbox surfaces, canonical summary builders, capability and role maps, and the related findings and guard tests
## Likely Affected Repo Surfaces
- Canonical findings status and workflow seams: `apps/platform/app/Models/Finding.php`, `apps/platform/app/Services/Findings/FindingWorkflowService.php`, `apps/platform/app/Policies/FindingPolicy.php`, and `apps/platform/database/factories/FindingFactory.php`
- Shared operator vocabulary seams: `apps/platform/app/Support/Badges/Domains/FindingStatusBadge.php`, `apps/platform/app/Support/Filament/FilterOptionCatalog.php`, `apps/platform/app/Support/Auth/Capabilities.php`, and `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
- Tenant findings Filament surfaces: `apps/platform/app/Filament/Resources/FindingResource.php`, `apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php`, `apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php`, and related workflow concerns
- Findings-derived summary and review helpers still relying on shared open-status handling or explicit `acknowledged` strings: `apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php`, `apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php`, `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`, `apps/platform/app/Support/Baselines/BaselineCompareStats.php`, and `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php`
- Query and generator consumers of `Finding::openStatusesForQuery()`: `apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php`, `apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php`, `apps/platform/app/Services/Baselines/BaselineAutoCloseService.php`, `apps/platform/app/Services/Findings/FindingAssignmentHygieneService.php`, `apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php`, and `apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php`
- Current proof surface likely requiring update or replacement: `apps/platform/tests/Feature/Models/FindingResolvedTest.php`, `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`, `apps/platform/tests/Feature/PermissionPosture/PermissionPostureFindingGeneratorTest.php`, `apps/platform/tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php`, `apps/platform/tests/Unit/Badges/FindingBadgesTest.php`, `apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php`, `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`, and `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
## Domain / Model Fit
- `Finding` remains the single tenant-owned source of truth with required `workspace_id` and `tenant_id` anchors. No new entity, enum family, compatibility table, or derived persistence layer is introduced.
- The canonical active lifecycle stays `new`, `triaged`, `in_progress`, and `reopened`, with `resolved`, `closed`, and `risk_accepted` remaining the canonical terminal set. `acknowledged` is removed as an active status contract rather than remapped through runtime helpers.
- `openStatusesForQuery()` and related status helpers should collapse onto the canonical open-status set instead of preserving an extra compatibility branch for `acknowledged`.
- Existing `acknowledged_at` and `acknowledged_by_user_id` columns are not justification for compatibility behavior in this slice. Default plan posture is to leave schema shape unchanged and remove productive semantics only.
- Legacy factory or fixture helpers that create `acknowledged` findings should be deleted or confined to explicitly documented stale-data edge proof only if implementation later proves that is still needed. They should not remain the default way to express current workflow truth.
## UI / Filament & Livewire Fit
- All touched operator surfaces remain native Filament v5 on Livewire v4. No custom dashboard framework, no panel change, and no new provider registration work are needed.
- `FindingResource` already has a `view` page, so the feature does not create a Filament global-search compliance problem. No new searchable resource is introduced.
- Shared badge rendering stays on `BadgeCatalog` plus `BadgeRenderer`, and shared filter vocabulary stays on `FilterOptionCatalog`; the cleanup must remove `acknowledged` from those shared paths rather than introducing page-local label overrides.
- Findings table, detail, and inbox surfaces should remove `acknowledged` from triage or progress visibility checks, filter options, summary counts, and helper wording together so operator UI does not drift between list, detail, and summary shells.
- No new destructive action is added. Existing destructive-like finding actions remain out of scope except that any touched action surface must preserve current `->requiresConfirmation()` and server-side capability or policy enforcement.
- No panel-only or shared asset changes are planned, so deployment keeps the existing `cd apps/platform && php artisan filament:assets` expectation unchanged only for already-registered assets.
## RBAC / Policy Fit
- Tenant membership and workspace membership remain the isolation boundaries: non-members stay `404`, entitled members missing the surviving capability stay `403`, and no resource existence leak is introduced while cleaning status language.
- `Capabilities::TENANT_FINDINGS_ACKNOWLEDGE` is removed instead of preserved as an alias. `RoleCapabilityMap`, `FindingPolicy`, and `FindingWorkflowService` should converge on the surviving findings capabilities only.
- The feature does not add a new role, a new authorization plane, or any page-local permission dialect. It narrows existing capability vocabulary.
- Disabled helper text and action affordances should continue to rely on the existing shared UI enforcement path while referencing only canonical findings workflow permissions.
## Audit / Logging Fit
- No new `AuditActionId` is introduced.
- Existing findings workflow audit verbs such as triage, assign, start progress, resolve, close, reopen, and risk acceptance remain canonical. The cleanup should not revive or add a finding-acknowledged audit dialect.
- Historical pre-production audit or metadata values do not justify runtime label shims. Verification-check acknowledgement audit behavior remains untouched.
## Migration / Data Shape Fit
- No new migration, no historical data backfill, and no fallback reader are planned.
- Repo evidence shows the findings status is a string column and the `acknowledged` behavior lives in code, factories, and tests rather than in a database enum or required migration path.
- Default implementation posture is to leave `findings` table columns intact for now and remove productive status compatibility from code and fixtures only. If schema removal appears necessary later, that must be split or re-justified instead of silently widening this cleanup.
- Local pre-production rows that still contain `acknowledged` are not a product compatibility requirement. Dev data reset or fixture replacement is preferred over runtime support.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed surfaces
- **Native vs custom classification summary**: native Filament plus shared badge, filter, and summary primitives
- **Shared-family relevance**: status messaging, findings workflow actions, shared badge semantics, shared filter vocabularies, canonical summary counts and previews
- **State layers in scope**: page, detail, URL-query
- **Audience modes in scope**: operator-MSP
- **Decision/diagnostic/raw hierarchy plan**: decision-first / diagnostics-second / support-raw-third
- **Raw/support gating plan**: existing capability-gated diagnostics remain unchanged; no new raw or support surface is introduced
- **One-primary-action / duplicate-truth control**: findings surfaces keep one canonical workflow language so triage and progress remain the dominant next actions without a duplicate `acknowledged` branch competing in badges, filters, or summaries
- **Handling modes by drift class or surface**: review-mandatory
- **Repository-signal treatment**: review-mandatory now; any leftover productive `acknowledged` findings seam after implementation is a blocker
- **Special surface test profiles**: standard-native-filament, global-context-shell
- **Required tests or manual smoke**: functional-core, state-contract
- **Exception path and spread control**: none; the removal must happen in the shared seams themselves rather than through local exemptions
- **Active feature PR close-out entry**: Guardrail
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: `Finding`, `FindingWorkflowService`, `FindingPolicy`, `FindingStatusBadge`, `FilterOptionCatalog`, `Capabilities`, `RoleCapabilityMap`, `FindingResource`, `ListFindings`, `MyFindingsInbox`, `WorkspaceHealthSummaryQuery`, `WorkspaceOverviewBuilder`, `GovernanceInboxSectionBuilder`, `BaselineCompareStats`, `TenantReviewSectionFactory`, and the generator or alert consumers of shared open-status helpers
- **Shared abstractions reused**: `BadgeCatalog`, `BadgeRenderer`, `FilterOptionCatalog`, shared findings status helpers, `UiEnforcement`, and the canonical capability registry
- **New abstraction introduced? why?**: none; the correct move is to remove one legacy branch from existing shared seams
- **Why the existing abstraction was sufficient or insufficient**: the shared seams are sufficient once the compatibility branch is removed centrally; they are the reason the cleanup cannot stay local to one page or test file
- **Bounded deviation / spread control**: none; any repo surface still naming productive findings `acknowledged` after the cleanup is drift and should be removed rather than wrapped
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: no
- **Central contract reused**: `N/A`
- **Delegated UX behaviors**: `N/A`
- **Surface-owned behavior kept local**: existing findings and summary surfaces only; no `OperationRun` start or link semantics change
- **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**: `N/A`
- **Platform-core seams**: existing findings, review, and governance vocabulary only
- **Neutral platform terms / contracts preserved**: existing platform and findings vocabulary remains; the cleanup narrows a finding-domain alias rather than spreading provider language
- **Retained provider-specific semantics and why**: none
- **Bounded extraction or follow-up path**: `N/A`
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first / snapshots-second: PASS - findings remain the last-observed tenant truth, and no backup or snapshot contract changes are introduced
- Read/write separation: PASS - the slice does not add a new write path; it only narrows status semantics on existing findings workflows
- Graph contract path: PASS - no Microsoft Graph contract or provider endpoint change is involved
- Deterministic capabilities: PASS - the capability registry becomes simpler by removing a stale alias instead of expanding capability derivation
- RBAC-UX and isolation: PASS - `/admin/t/{tenant}` findings surfaces remain tenant-scoped; non-members stay `404`; in-scope members missing surviving findings capability stay `403`; no raw capability strings should survive after cleanup
- Workspace isolation / tenant isolation: PASS - tenant-owned findings and derived canonical summaries keep current workspace plus tenant entitlement rules
- Destructive confirmation standard: PASS - no new destructive action is introduced; any touched destructive-like findings action must preserve current confirmation and authorization semantics
- Global search safety: PASS - `FindingResource` already has a view page, and no new searchable resource is added
- OperationRun observability and Ops-UX: PASS - no new operation type, run start surface, or run-notification path is introduced
- Data minimization: PASS - no new payload, no raw evidence expansion, and no new audit family is introduced
- Test governance (`TEST-GOV-001`): PASS - proof stays in focused unit plus feature coverage with one explicit retained heavy-governance guard layer for shared badge or filter drift
- Proportionality (`PROP-001`) and no premature abstraction (`ABSTR-001`): PASS - the plan is subtractive and introduces no new abstraction, registry, or semantic framework
- Persisted truth (`PERSIST-001`): PASS - no new table, entity, artifact, or stored compatibility layer is added
- Behavioral state (`STATE-001`): PASS - the feature removes a legacy active-workflow branch instead of adding a new state family
- UI semantics (`UI-SEM-001`) and shared pattern first (`XCUT-001`): PASS - badge, filter, workflow, and summary semantics stay on existing shared seams rather than a new interpretation layer
- Provider boundary (`PROV-001`) and few layers (`V1-EXP-001`, `LAYER-001`): PASS - no provider seam or extra layer is introduced
- Filament-native UI and planning contract: PASS - Filament v5 remains on Livewire v4, provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, and no panel or asset strategy change is required
- Asset strategy: PASS - no new panel or shared asset registration is planned; existing deploy behavior for `filament:assets` remains unchanged
## Test Governance Check
- **Test purpose / classification by changed surface**: Unit for findings status helpers, badge or filter catalog semantics, and capability-registry cleanup; Feature for findings workflow actions, resource or inbox behavior, policy outcomes, and shared summary consumers; Heavy-Governance for the explicit guard layer that blocks ad-hoc status badge or table drift
- **Affected validation lanes**: fast-feedback, confidence, heavy-governance
- **Why this lane mix is the narrowest sufficient proof**: the core risk is central semantic drift across shared helpers and operator surfaces, not browser choreography or new async behavior. Focused unit and feature coverage prove the canonical status path, while one retained guard layer ensures `acknowledged` does not reappear through shared UI seams
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Findings/FindingStatusSemanticsTest.php tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/PermissionPostureFindingGeneratorTest.php tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php tests/Feature/Findings/FindingsIntakeQueueTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
- **Fixture / helper / factory / seed / context cost risks**: acknowledged-specific fixtures and factory states are likely removal targets; keep any remaining stale-data setup explicit and local instead of spreading a legacy default across helper layers
- **Expensive defaults or shared helper growth introduced?**: no; expected net-neutral to negative because the slice removes compatibility branches and stale test setup
- **Heavy-family additions, promotions, or visibility changes**: no new heavy family is planned; one existing shared guard layer remains explicit because badge and filter drift can otherwise reintroduce removed semantics silently
- **Surface-class relief / special coverage rule**: standard-native-filament and global-context-shell relief are sufficient; no browser lane is required for this cleanup
- **Closing validation and reviewer handoff**: rerun the focused commands above, verify that no productive findings seam, capability alias, badge label, filter option, or summary builder still treats `acknowledged` as current workflow truth, and verify that verification or onboarding acknowledgement domains remain unchanged
- **Budget / baseline / trend follow-up**: expected net-neutral to slightly negative because compatibility-only tests should be removed or consolidated
- **Review-stop questions**: did implementation leave `acknowledged` in a shared status helper, a policy or capability path, a badge or filter catalog, a summary builder, or a generator test family; did it widen into schema removal or non-finding acknowledgement work
- **Escalation path**: reject-or-split
- **Active feature PR close-out entry**: Guardrail
- **Why no dedicated follow-up spec is needed**: this slice is a bounded cleanup; creation-time invariant hardening, backfill-runtime-surface removal, and external support handoff already remain explicit separate follow-up work
## Rollout & Risk Controls
- Implement as replacement, not aliasing. Shared helpers, capability registries, and summary builders should converge directly on canonical statuses in the same slice.
- Treat local stale `acknowledged` rows as pre-production cleanup debt, not a customer compatibility contract. Do not add fallback readers or UI labels to preserve them.
- Preserve scope boundaries aggressively: verification acknowledgement, onboarding verification acknowledgement, restore impact acknowledgement, and non-finding support acknowledgement semantics stay untouched.
- Review stop conditions should fire if implementation tries to drop schema, invent a new compatibility mapper, or widen into findings lifecycle redesign.
- Rollout is code-only and repo-local. No queue, deployment, asset, or migration sequencing is expected.
## Project Structure
### Documentation (this feature)
```text
specs/254-remove-acknowledged-compat/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── checklists/
│ └── requirements.md
├── contracts/
│ └── findings-acknowledged-compat-removal.contract.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/Findings/
│ │ └── Resources/FindingResource/
│ ├── Jobs/Alerts/
│ ├── Models/
│ ├── Policies/
│ ├── Services/
│ │ ├── Auth/
│ │ ├── Baselines/
│ │ ├── EntraAdminRoles/
│ │ ├── Findings/
│ │ ├── PermissionPosture/
│ │ └── TenantReviews/
│ └── Support/
│ ├── Auth/
│ ├── Badges/
│ ├── CustomerHealth/
│ ├── Filament/
│ ├── GovernanceInbox/
│ └── Workspaces/
├── database/
│ └── factories/
└── tests/
├── Feature/
│ ├── Auth/
│ ├── Findings/
│ ├── Guards/
│ ├── PermissionPosture/
│ └── EntraAdminRoles/
└── Unit/
├── Badges/
└── Findings/
```
**Structure Decision**: Laravel monolith. Implementation should stay inside existing findings model, workflow, policy, shared support, Filament resource or page, factory, and test directories rather than creating a new namespace or migration track.
## Complexity Tracking
No constitution violation is expected. If implementation later proves it needs schema removal, a compatibility shim, or a new translation layer, that is a stop condition and should be split or rejected rather than absorbed here.
## Proportionality Review
N/A - this slice removes a legacy active-workflow alias and a stale capability alias. It introduces no new enum, presenter, persistence, contract layer, or taxonomy.
## Phase 0 — Research (output: `research.md`)
- Confirm the exact productive findings seams that still use `acknowledged` and separate them from explicitly out-of-scope non-finding acknowledgement domains.
- Confirm the no-migration posture for existing `acknowledged_*` fields and local stale rows under LEAN-001.
- Confirm which summary or review helpers still encode literal `acknowledged` status expectations and therefore belong in this cleanup instead of a later lifecycle redesign.
- Confirm which existing tests should be deleted, narrowed, or rewritten rather than preserved as compatibility proof.
## Phase 1 — Design & Contracts (outputs: `data-model.md`, `contracts/`, `quickstart.md`)
- `data-model.md` should describe the surviving canonical findings status set, the collapsed open-status query contract, the removal of the acknowledge capability alias, and the unchanged schema posture.
- `contracts/findings-acknowledged-compat-removal.contract.yaml` should capture the cleanup matrix across model helpers, workflow and policy authorization, shared badge or filter catalogs, findings resource behavior, summary consumers, and out-of-scope acknowledgement domains.
- `quickstart.md` should document the intended implementation order, validation commands, and review stop conditions for scope drift.
## Phase 1 — Agent Context Update
After Phase 1 artifacts are generated, update Copilot context from the plan:
- `/Users/ahmeddarrazi/Documents/projects/wt-plattform/.specify/scripts/bash/update-agent-context.sh copilot`
## Phase 2 — Implementation Outline (tasks created in `/speckit.tasks`)
- Collapse `Finding` status helpers and legacy model helpers onto the canonical status set only.
- Remove `TENANT_FINDINGS_ACKNOWLEDGE` and update role mappings, workflow authorization, and policy checks to the surviving capability set.
- Remove `acknowledged` from shared badge and filter catalogs and from findings resource or inbox workflow affordances.
- Update shared summary and generator consumers of `openStatusesForQuery()` or explicit `acknowledged` status lists so counts, previews, and auto-close behavior align with canonical statuses.
- Delete or rewrite acknowledged-compatibility tests and factories, then add focused regression proof for the surviving canonical workflow, summary alignment, and guard coverage.
- Verify that no non-finding acknowledgement domains were touched and that no migration or compatibility shim was introduced.
## Constitution Check (Post-Design)
Re-check target: PASS. The post-design shape must stay subtractive, keep Filament v5 on Livewire v4, leave provider registration unchanged in `apps/platform/bootstrap/providers.php`, keep `FindingResource` global-search-safe through its existing view page, add no new destructive action or asset bundle, and preserve the no-migration, no-compatibility-shim posture.

View File

@ -0,0 +1,36 @@
# Quickstart — Remove Legacy Acknowledged Finding Status Compatibility
## Prereqs
- Docker running
- Laravel Sail dependencies installed
- Existing findings, RBAC, summary, and generator test fixtures available
- Existing seeded tenant/workspace context for targeted findings workflow tests
## Run locally
- Start containers: `cd apps/platform && ./vendor/bin/sail up -d`
- No schema change is expected, but use the normal repo baseline before running tests: `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan migrate --no-interaction`
- Run targeted tests after implementation:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php tests/Unit/Findings/FindingStatusSemanticsTest.php tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php tests/Feature/Baselines/BaselineCompareStatsTest.php tests/Feature/Alerts/SlaDueAlertTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PermissionPosture/PermissionPostureFindingGeneratorTest.php tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php tests/Feature/Findings/FindingsIntakeQueueTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
- Format after implementation: `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
## Manual smoke after implementation
1. Sign in to `/admin/t/{tenant}/findings` as an entitled tenant operator and confirm the findings register and detail use canonical status badges, filters, helper text, and workflow wording only.
2. Exercise canonical findings actions such as `Triage`, `Start progress`, `Assign`, `Resolve`, `Close`, and `Risk accept` and confirm no action or helper text refers to an acknowledge alias.
3. Open the affected canonical `/admin` summary and inbox surfaces and confirm counts and previews match the same canonical open findings set as the findings register.
4. Open an in-scope tenant review, review-pack, or support-diagnostic surface that renders findings-derived open-work disclosure and confirm it does not describe `acknowledged` as current work.
5. Verify capability-driven findings gating no longer references `tenant_findings.acknowledge` while preserving existing `404` versus `403` behavior.
6. Review the diff and confirm no file under `apps/platform/database/migrations/` changed and no new persisted compatibility artifact was introduced.
## Notes
- Filament v5 remains on Livewire v4.0+ in this repo; the cleanup stays inside existing native Filament resources, pages, and shared support helpers.
- No panel or provider registration changes are planned; `apps/platform/bootstrap/providers.php` remains authoritative if provider work is ever needed later.
- `FindingResource` already has a view page, so the feature does not create a global-search contract issue.
- No asset changes are expected, so there is no additional `filament:assets` deployment work for this slice.
- LEAN-001 applies directly: remove compatibility branches instead of preserving aliases, fallback readers, or migrations for historical pre-production rows.

View File

@ -0,0 +1,129 @@
# Research — Remove Legacy Acknowledged Finding Status Compatibility
**Date**: 2026-04-29
**Spec**: [spec.md](spec.md)
This document records the repo-grounded planning decisions for the acknowledged-compatibility cleanup slice. All decisions assume the current pre-production LEAN-001 posture.
## Decision 1 — Remove acknowledged semantics at shared seams, not through page-local relabeling
**Decision**: Delete productive `acknowledged` compatibility from the shared findings seams that currently define status truth, query truth, badge vocabulary, filter vocabulary, workflow eligibility, and capability language. Do not treat this as a page-local label replacement.
**Rationale**:
- The drift is cross-surface today: `Finding::STATUS_ACKNOWLEDGED`, `Finding::openStatusesForQuery()`, `FilterOptionCatalog`, `BadgeCatalog`, `FindingWorkflowService`, `Capabilities::TENANT_FINDINGS_ACKNOWLEDGE`, and multiple summary consumers all preserve the old vocabulary.
- A list-only or badge-only rename would leave summary counts, disabled helper text, and RBAC wording inconsistent.
- XCUT-001 requires converging on the existing shared path instead of adding local exceptions.
**Evidence**:
- `apps/platform/app/Models/Finding.php`
- `apps/platform/app/Services/Findings/FindingWorkflowService.php`
- `apps/platform/app/Support/Filament/FilterOptionCatalog.php`
- `apps/platform/app/Support/Badges/Domains/FindingStatusBadge.php`
- `apps/platform/app/Support/Auth/Capabilities.php`
- `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
**Alternatives considered**:
- Relabel `acknowledged` to `triaged` only on findings pages.
- Rejected: shared queries, summaries, and capability guidance would still preserve conflicting truth.
- Keep a read-side compatibility mapper indefinitely.
- Rejected: LEAN-001 forbids preserving a pre-production legacy branch without a current-release need.
## Decision 2 — Treat stale acknowledged rows and columns as pre-production residue, not as a runtime compatibility contract
**Decision**: Keep the cleanup code-only by default. Remove productive semantics first and do not add migration shims, fallback readers, or preserved UI labels just because `acknowledged_at` or `acknowledged_by_user_id` columns still exist locally.
**Rationale**:
- The repo is explicitly pre-production, and LEAN-001 prefers replacement or deletion over historical compatibility behavior.
- The current problem is active workflow semantics in code and tests, not an unavoidable database constraint.
- The narrowest correct implementation is to stop writing, querying, and presenting `acknowledged` as current findings truth.
**Evidence**:
- `apps/platform/app/Models/Finding.php`
- `.specify/memory/constitution.md` (LEAN-001, PERSIST-001)
- `docs/product/spec-candidates.md`
**Alternatives considered**:
- Add a migration or fallback reader now.
- Rejected: widens scope into persistence work not justified by current release truth.
- Preserve `legacy acknowledged` UI labels until later.
- Rejected: keeps the removed semantics productized.
## Decision 3 — Keep findings-derived review, report, and support-diagnostic consumers in scope where they surface current open-work truth
**Decision**: Include review/report and support-diagnostic consumers in this cleanup only where they derive current findings-open counts, disclosure text, or issue grouping from the same shared status helpers as canonical summaries.
**Rationale**:
- Repo truth shows `TenantReviewSectionFactory` and `SupportDiagnosticBundleBuilder` still depend on acknowledged-aware status logic.
- Leaving those consumers out would preserve productive status drift even if the findings register and canonical `/admin` summaries were cleaned.
- This remains bounded because the slice is limited to findings-derived open-work semantics, not broader review-pack, evidence, or diagnostic redesign.
**Evidence**:
- `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php`
- `apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php`
- `apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php`
- `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`
**Alternatives considered**:
- Defer review/report and diagnostics entirely.
- Rejected: current productive consumers would still present split workflow truth.
- Broaden into review-pack or diagnostic domain redesign.
- Rejected: outside the smallest cleanup slice.
## Decision 4 — Keep non-finding acknowledgement domains explicitly out of scope
**Decision**: Do not rename or remove acknowledgement semantics outside the findings domain, including verification-check acknowledgement, onboarding-verification acknowledgement, and restore impact acknowledgement.
**Rationale**:
- Those domains carry different user intent and do not prove that findings status compatibility must remain.
- Mixing them into this slice would widen terminology cleanup into unrelated workflows.
- The spec already depends on maintaining bounded ownership and avoiding accidental cross-domain churn.
**Evidence**:
- `apps/platform/app/Services/Verification/VerificationCheckAcknowledgementService.php`
- `apps/platform/app/Filament/Support/VerificationReportViewer.php`
- `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`
- `apps/platform/app/Filament/Resources/RestoreRunResource.php`
**Alternatives considered**:
- Normalize every `acknowledge*` term in the repo at once.
- Rejected: too broad and not required for the findings cleanup to be correct.
## Decision 5 — Keep validation in focused Feature, Unit, and retained guard lanes only
**Decision**: Prove the cleanup with focused findings workflow tests, focused summary-consumer tests, focused capability cleanup tests, and the already-retained heavy-governance guard coverage. Do not add browser coverage.
**Rationale**:
- The business risk is shared-seam drift, not browser choreography or async execution.
- The repo already has meaningful findings, generator, summary, and guard test families that can be narrowed or rewritten.
- TEST-GOV-001 prefers the smallest proving lane mix that guards business truth.
**Evidence**:
- `apps/platform/tests/Feature/PermissionPosture/PermissionPostureFindingGeneratorTest.php`
- `apps/platform/tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php`
- `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`
- `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php`
- `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
**Alternatives considered**:
- Add browser smoke coverage.
- Rejected: low additional value for this cleanup.
- Preserve broad acknowledged-compatibility fixture families.
- Rejected: would keep the removed semantics alive in the suite.
## Decision 6 — Remove the stale findings capability alias instead of translating it forever
**Decision**: Delete `tenant_findings.acknowledge` from the canonical capability registry and role mappings, and converge disabled helper text and authorization expectations on the surviving findings permissions.
**Rationale**:
- The acknowledged alias keeps RBAC language inconsistent with the canonical triage action.
- Capability drift is part of the user-visible problem in this slice, not a separate concern.
- RBAC-UX requires server-side truth to stay on the canonical capability set, not parallel aliases.
**Evidence**:
- `apps/platform/app/Support/Auth/Capabilities.php`
- `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
- `apps/platform/app/Policies/FindingPolicy.php`
**Alternatives considered**:
- Keep the alias as an undocumented backward-compatibility seam.
- Rejected: preserves the exact semantics blocker this feature is intended to remove.

View File

@ -0,0 +1,283 @@
# Feature Specification: Remove Legacy Acknowledged Finding Status Compatibility
**Feature Branch**: `254-remove-acknowledged-compat`
**Created**: 2026-04-29
**Status**: Draft
**Input**: User description: "Prepare the next repo-based cleanup slice that removes legacy acknowledged finding-status compatibility and collapses findings workflow semantics onto canonical triaged or open handling without changing customer-facing workflow scope or reintroducing repair tooling."
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: TenantPilot still carries two parallel findings workflow languages. The product treats `triaged` as the canonical operator meaning, but productive code, shared queries, status filters, badges, role mappings, and workflow tests still preserve `acknowledged` compatibility as if it were an active workflow truth.
- **Today's failure**: Operators and maintainers can still encounter `acknowledged` as a current finding status through shared helpers, filter options, badge labels, capability aliases, and findings-derived summary logic. That weakens workflow clarity, keeps RBAC language inconsistent, and makes shared counts and previews harder to trust.
- **User-visible improvement**: Tenant and workspace operators see one canonical findings workflow language. Status badges, filters, summary counts, helper text, and workflow actions consistently speak in `new`, `triaged`, `in_progress`, `reopened`, `resolved`, `closed`, and `risk_accepted` terms only.
- **Smallest enterprise-capable version**: Remove acknowledged compatibility end to end from productive findings status constants and helpers, shared query helpers, shared filter and badge catalogs, capability registry and role mappings, workflow-facing tests, and findings-derived summary surfaces while leaving the rest of the findings lifecycle unchanged.
- **Explicit non-goals**: No backfill-runtime-surface removal in this slice, no broader findings lifecycle redesign, no new states, no migration shim, no historical data migration, no verification-acknowledgement cleanup, no onboarding-verification terminology rewrite, and no new customer-facing workflow surface.
- **Permanent complexity imported**: Net negative. The slice removes a legacy status branch, a capability alias, catalog special-casing, and acknowledged-specific workflow expectations. The only enduring obligation is focused regression coverage that proves one canonical status path remains across findings workflows and findings-derived summaries.
- **Why now**: This candidate remains explicitly open in both product sources and is still repo-proven in productive code. It is smaller and more implementation-ready than creation-time invariant hardening, and it does not depend on an external product decision like External Support Desk / PSA Handoff.
- **Why not local**: The compatibility drift is not confined to one screen or helper. It spans the `Finding` model, workflow service, shared filter catalog, badge language, capability registry, role mappings, canonical summary builders, and workflow-facing tests. A local rename would leave inconsistent product truth in other entry points.
- **Approval class**: Cleanup
- **Red flags triggered**: Multiple micro-specs for one domain. Defense: this slice is intentionally limited to canonical status and RBAC vocabulary cleanup only, while creation-time invariants and external support handoff remain explicit follow-up candidates.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 11/12**
- **Decision**: approve
## Selection Rationale
- `Remove Legacy Acknowledged Finding Status Compatibility` is still active in [docs/product/spec-candidates.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/docs/product/spec-candidates.md#L172) and [docs/product/implementation-ledger.md](/Users/ahmeddarrazi/Documents/projects/wt-plattform/docs/product/implementation-ledger.md#L183) and remains a concrete semantics blocker instead of a speculative cleanup.
- Repo truth still shows acknowledged drift in the canonical findings model and workflow seams, including `Finding::STATUS_ACKNOWLEDGED`, `Finding::openStatusesForQuery()`, `FilterOptionCatalog::findingStatuses()`, `Capabilities::TENANT_FINDINGS_ACKNOWLEDGE`, and findings-derived summary builders.
- This slice is narrower and safer than `Enforce Creation-Time Finding Invariants` because it removes visible and shared workflow ambiguity first without widening generator hardening or recurrence rules.
- `Cross-Tenant Compare and Promotion v1` is not the next preparation target here because the repo already has refreshed Spec 043 ready for later implementation work.
- `External Support Desk / PSA Handoff` stays deferred because it still depends on a concrete external-desk target and broader commercialization workflow decisions, while this cleanup is fully repo-based today.
## Spec Scope Fields *(mandatory)*
- **Scope**: tenant + canonical-view
- **Primary Routes**:
- `/admin/t/{tenant}/findings`
- `/admin/t/{tenant}/findings/{record}`
- existing canonical `/admin` summary and inbox surfaces that derive open-finding counts or previews from shared findings queries
- existing findings table and filter surfaces that use shared finding-status catalog options
- existing tenant review, review-pack, and support-diagnostic surfaces only where they render findings-derived open-work summaries, counts, or disclosure text
- **Data Ownership**:
- Tenant-owned `Finding` and related `FindingException` truth remain canonical and keep required `workspace_id` plus `tenant_id` anchors.
- Workspace, canonical summary, review/report, and diagnostic consumers stay derived over tenant-owned findings truth; this feature introduces no new persistence, no mirror entity, and no migration data store.
- Historical pre-production findings rows do not justify a compatibility table, alias, or fallback reader.
- **RBAC**:
- Tenant membership remains the isolation boundary for findings visibility and surviving finding workflow mutations.
- Canonical findings workflow permissions stay capability-first and tenant-scoped; `tenant_findings.acknowledge` is removed rather than preserved as an alias.
- Non-members remain deny-as-not-found and entitled members missing surviving findings capabilities remain forbidden on the affected mutation paths.
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: Canonical `/admin` summary and inbox surfaces that launch from tenant context continue to prefilter to the current tenant, but they must do so with canonical open-status handling only and without an acknowledged compatibility branch.
- **Explicit entitlement checks preventing cross-tenant leakage**: Existing workspace membership and visible-tenant filtering remain authoritative on canonical summary surfaces. The cleanup must not widen findings queries or previews beyond entitled tenants while removing acknowledged compatibility.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: status messaging, workflow helper text, shared badges, shared filter vocabularies, canonical summary counts and previews, capability language
- **Systems touched**: `App\Models\Finding`, `App\Services\Findings\FindingWorkflowService`, `App\Support\Filament\FilterOptionCatalog`, `App\Support\Auth\Capabilities`, `App\Services\Auth\RoleCapabilityMap`, existing findings resource surfaces, existing findings-derived canonical summary builders, `App\Services\TenantReviews\TenantReviewSectionFactory`, and `App\Support\SupportDiagnostics\SupportDiagnosticBundleBuilder`
- **Existing pattern(s) to extend**: shared finding-status helpers, shared filter and badge catalogs, existing findings workflow actions, existing capability registry, and existing canonical summary builders remain the only supported paths
- **Shared contract / presenter / builder / renderer to reuse**: `Finding::openStatuses()`, shared `BadgeCatalog` finding-status semantics, `FilterOptionCatalog::findingStatuses()`, the canonical capability registry, and existing summary builders that already derive open findings from shared helpers
- **Why the existing shared path is sufficient or insufficient**: the shared paths are sufficient once the legacy acknowledged branch is removed. They are the reason this cleanup must land centrally instead of through page-local exceptions or label overrides.
- **Allowed deviation and why**: none
- **Consistency impact**: triage wording, open counts, badges, filters, review/report disclosure text, diagnostic issue summaries, disabled helper text, and role guidance must all converge on the same canonical finding-status language in the same slice.
- **Review focus**: reviewers must verify that no productive code path, shared filter, badge label, capability alias, review/report disclosure, diagnostic summary, or workflow-facing test still treats `acknowledged` as current findings workflow truth.
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
- **Touches OperationRun start/completion/link UX?**: no
- **Shared OperationRun UX contract/layer reused**: `N/A`
- **Delegated start/completion UX behaviors**: `N/A`
- **Local surface-owned behavior that remains**: existing findings and summary surfaces remain read/write or read-only according to their current workflows; no `OperationRun` start semantics are introduced or removed by this status cleanup.
- **Queued DB-notification policy**: `N/A`
- **Terminal notification path**: `N/A`
- **Exception required?**: none
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
N/A - no shared provider or platform seam is widened. This slice only removes legacy findings workflow compatibility inside the existing findings domain.
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Tenant findings register and detail: remove acknowledged wording from statuses, filters, and workflow affordances | yes | Native Filament + shared workflow primitives | row actions, header actions, badges, filters | list, detail, action state | no | Canonical triage language only |
| Canonical findings-derived summaries: governance inbox, workspace overview, customer health, and similar previews use canonical open-status handling only | yes | Native Filament + shared summary builders | dashboard cards, inbox previews, counters, drilldowns | page, widget, query state | no | Summary counts and previews stop carrying a hidden acknowledged branch |
| Shared findings status filters and badges: remove legacy acknowledged option and label | yes | Shared badge and filter catalog primitives | status messaging, filter vocabulary, badge semantics | catalog, table filter state | no | No `legacy acknowledged` affordance remains on productive findings surfaces |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Tenant findings register and detail | Primary Decision Surface | Decide how to triage or continue work on a finding | canonical status, severity, due or SLA signals, responsibility, and canonical workflow actions | evidence, history, related runs, and exception detail after opening the finding | Primary because this is where tenant operators act on findings today | Keeps findings work centered on one canonical lifecycle path | Removes a parallel acknowledged label that competes with the real next action |
| Canonical findings-derived summaries | Secondary Context Surface | Decide where follow-up exists before drilling into findings work | counts, previews, and urgency signals derived from canonical open statuses only | the findings register or detail after navigation | Not primary because these surfaces route operators into findings work rather than owning the mutations themselves | Keeps overview or inbox surfaces honest about what is actually open | Removes mismatched counts and pseudo-open summary branches |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Tenant findings register and detail | operator-MSP | canonical findings status, responsibility, due state, and workflow affordances | history, evidence, exception detail, and related runs after opening the record | raw or support-only detail remains on existing deeper routes and capability gates | `Triage finding` or continue canonical workflow | low-level evidence and audit detail stay secondary | status is stated once in canonical terms and deeper sections add evidence rather than alternate vocabulary |
| Canonical findings-derived summaries | operator-MSP | canonical counts, previews, and urgency signals only | secondary drilldowns to the findings register and detail | raw evidence is never the default content on the summary surface | `Open findings` | detailed evidence and audit context stay on deeper surfaces | summaries describe open work once and rely on the findings register for detailed truth |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Tenant findings register and detail | List / Table / Bulk | CRUD / List-first Resource | Open a finding and continue the canonical workflow | full-row navigation to finding detail | required | existing row `More` actions and detail-header actions only | existing destructive-like actions remain in grouped or detail-header placements | `/admin/t/{tenant}/findings` | `/admin/t/{tenant}/findings/{record}` | tenant context, status filters, responsibility, due state | Findings / Finding | canonical findings workflow state and urgency | none |
| Canonical findings-derived summaries | Monitoring / Queue / Workbench | Context-first summary and preview shell | open a filtered findings view for the relevant tenant or queue | card or preview drilldown into the findings register | forbidden | secondary links only | none | existing canonical `/admin` summary and inbox pages | `/admin/t/{tenant}/findings` | workspace context, tenant filter, preview scope | Findings follow-up / Findings follow-up | where open work exists under the canonical status set | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Tenant findings register and detail | Tenant operator | Decide how to triage, assign, continue, resolve, close, or risk-accept a finding | List/detail | What should I do next with this finding? | canonical status, severity, responsibility, due or SLA state, and current workflow affordances | evidence, exception history, audit context, related operations | lifecycle, urgency, responsibility, governance validity | TenantPilot only for the existing findings workflow actions | Triage, Start progress, Assign, Resolve, Risk accept | existing destructive-like workflow actions only |
| Canonical findings-derived summaries | Workspace or portfolio operator | Decide where follow-up exists before drilling into findings work | Summary and preview | Where is open findings work waiting right now? | canonical counts, previews, due urgency, and tenant context | deeper findings detail after navigation | lifecycle and urgency only | none on the summary surface itself | Open findings | none |
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Feature, Unit, Heavy-Governance
- **Validation lane(s)**: fast-feedback, confidence, heavy-governance
- **Why this classification and these lanes are sufficient**: focused feature coverage proves canonical findings workflows and findings-derived summaries stop exposing acknowledged semantics, while narrow unit coverage proves the shared status helper, filter catalog, and capability cleanup stay centralized. One retained heavy-governance guard layer remains appropriate because status-like badge/filter drift can reappear through shared seams even after the main behavior is corrected.
- **New or expanded test families**: focused findings workflow cleanup coverage, focused findings-summary consistency coverage, and bounded filter or capability cleanup coverage. No browser family is introduced.
- **Fixture / helper cost impact**: low and likely net-neutral to slightly negative. Acknowledged-only fixtures and compatibility expectations should be consolidated or deleted instead of adding new heavy setup.
- **Heavy-family visibility / justification**: retained shared guard coverage for status-like tokens and filter-catalog usage remains explicit so a local reintroduction of acknowledged semantics cannot survive through shared UI seams. No new heavy-governance family is introduced.
- **Special surface test profile**: standard-native-filament, global-context-shell
- **Standard-native relief or required special coverage**: standard Filament and domain coverage are sufficient for the findings resource and canonical summaries. Required extra proof is shared guard coverage for badge and filter drift.
- **Reviewer handoff**: reviewers must confirm that acknowledged disappears together from the model helper, workflow rules, shared filter options, shared badge language, role/capability vocabulary, and summary counts or previews. They must also confirm that verification acknowledgement and onboarding acknowledgement remain untouched.
- **Budget / baseline / trend impact**: net-neutral to slightly negative because the slice should remove acknowledged-only compatibility expectations rather than widen the suite.
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **Planned validation commands**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Findings/FindingStatusSemanticsTest.php tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
## RBAC / Isolation Considerations
- Tenant findings mutations remain tenant-scoped and follow existing deny-as-not-found versus forbidden semantics: non-members remain `404`, entitled members missing a surviving findings capability remain `403` on mutation.
- Canonical `/admin` summary and inbox surfaces continue to derive only from tenants the actor can see in the current workspace. Removing acknowledged compatibility must not broaden canonical previews or counts beyond the current visible-tenant boundary.
- `tenant_findings.acknowledge` is removed rather than preserved as an alias. Canonical findings workflow language stays capability-first and centered on the surviving findings capabilities.
- This slice does not add a new role family, a new authorization plane, or a new hidden compatibility bypass.
## Auditability
- No new audit action ID is introduced by this cleanup.
- Existing findings workflow audit actions such as triage, assignment, in-progress, resolve, close, reopen, and risk acceptance remain the canonical audit language for surviving workflow actions.
- Pre-production historical acknowledgement fields or audit rows do not justify a compatibility renderer, label shim, or preserved active-workflow vocabulary.
- The implementation should ensure that operator-facing surfaces do not continue to advertise `acknowledged` as a current workflow outcome merely because historical audit or fixture data once used it.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Use One Canonical Findings Workflow Language (Priority: P1)
As a tenant operator, I want findings surfaces to speak in one canonical workflow language so I can triage and continue work without guessing whether `acknowledged` and `triaged` still mean different things.
**Why this priority**: This is the core user-facing value of the cleanup. If findings still speak two different workflow languages, the slice has failed.
**Independent Test**: Open the findings register and detail for a tenant with open findings and verify that statuses, filters, badges, and workflow affordances use canonical findings vocabulary only.
**Acceptance Scenarios**:
1. **Given** an entitled tenant operator opens the findings register, **When** the page renders, **Then** no current status badge, filter option, or helper text exposes `acknowledged` as a valid findings workflow state.
2. **Given** an open finding is ready for triage or progress, **When** the operator uses the surviving workflow affordances, **Then** the workflow uses canonical `triaged` semantics rather than an acknowledge alias.
3. **Given** a finding has already reached a terminal state, **When** the operator opens it, **Then** the terminal states remain unchanged and no new replacement state is introduced.
---
### User Story 2 - Keep Summary Counts, Reports, and Diagnostics Honest (Priority: P1)
As a workspace or portfolio operator, I want shared counts, previews, review/report disclosures, and diagnostic summaries to match the findings register so I can trust what is actually open without a hidden acknowledged branch skewing the numbers.
**Why this priority**: Shared summary drift is one of the main reasons this cleanup cannot stay local. If summaries, review/report disclosures, diagnostics, and lists diverge, operators lose trust in the overview surfaces.
**Independent Test**: Compare the findings register with the affected canonical summary and inbox surfaces plus findings-derived review/report and support-diagnostic consumers for the same tenant or workspace context and verify that the same canonical open-status set drives all of them.
**Acceptance Scenarios**:
1. **Given** a canonical summary surface shows open findings work for a tenant, **When** the operator drills into the findings register, **Then** the summary counts and previews align with the same canonical open-status set.
2. **Given** the operator changes tenant or workspace context, **When** canonical summaries reload, **Then** they continue to derive findings work only from canonical open statuses and the currently entitled tenant set.
3. **Given** a tenant review, review-pack, or support-diagnostic surface renders findings-derived open-work disclosure, **When** the operator opens that surface, **Then** the disclosure text, counts, and issue grouping use the same canonical open-status set and do not present `acknowledged` as current work.
---
### User Story 3 - Keep RBAC Language Canonical (Priority: P2)
As a tenant manager or owner, I want role guidance and capability-driven findings actions to use one canonical permission language so workflow help and disabled states stay understandable.
**Why this priority**: The acknowledged alias is not only a status problem. It also keeps role and action guidance inconsistent across the same workflow.
**Independent Test**: Review capability-driven findings surfaces and role expectations after the cleanup and verify that they reference surviving findings capabilities only.
**Acceptance Scenarios**:
1. **Given** a tenant member can triage findings, **When** the findings UI explains or gates the action, **Then** it refers to the canonical triage capability and not an acknowledge alias.
2. **Given** a tenant member lacks the required surviving capability, **When** they attempt the affected findings action, **Then** the existing forbidden behavior remains and no acknowledge-specific permission branch is used.
### Edge Cases
- Local or historical pre-production rows may still contain `acknowledged`; this slice does not add a migration shim, fallback reader, or preserved UI label to keep that branch alive.
- Removing acknowledged from shared helpers must update list surfaces, summary counts, preview queries, filters, and badges together; otherwise the cleanup would create a new mismatch instead of removing one.
- Verification-check acknowledgement and onboarding-verification acknowledgement are separate domains and must not be renamed or removed as collateral damage in this slice.
- Resolved, closed, and risk-accepted findings behavior remains distinct and must not be collapsed while removing the acknowledged compatibility path.
## Requirements *(mandatory)*
**Constitution alignment (LEAN-001 / STATE-001 / SPEC-GATE-001):** This is a pre-production cleanup slice. It removes a legacy findings workflow branch rather than introducing new state, persistence, or abstraction. Compatibility shims, fallback readers, historical fixture preservation, and capability aliases are out of scope.
**Constitution alignment (XCUT-001 / BADGE-001):** Because the drift survives in shared status helpers, shared filter catalogs, shared badge language, and shared summary builders, the cleanup must land through the shared paths themselves. No page-local override or secondary presenter may keep acknowledged alive.
**Constitution alignment (RBAC-UX):** Tenant membership and capability rules stay unchanged except for removing the acknowledged alias from the findings capability vocabulary. Non-members remain `404`; entitled members missing the surviving capability remain `403` on mutations.
**Constitution alignment (TEST-GOV-001):** Proof stays in focused feature, unit, and bounded heavy-governance guard coverage. The slice should remove acknowledged-only expectations rather than creating a broader or heavier new test family.
**Constitution alignment (UI-FIL-001 / UI-NAMING-001 / DECIDE-001 / OPSURF-001):** Findings surfaces remain native Filament or shared summary surfaces. Canonical findings vocabulary must stay consistent across badges, filters, helper text, row actions, disabled states, and drilldown summaries without introducing a new local status language.
### Functional Requirements
- **FR-254-001**: The system MUST retire `acknowledged` as a productive findings workflow status and remove any status helper that treats it as a current canonical findings state.
- **FR-254-002**: Shared open-status query helpers and findings-derived summary builders MUST rely on the canonical open findings status set only and MUST NOT preserve a hidden acknowledged compatibility branch.
- **FR-254-003**: Shared findings filter catalogs, status badges, and related helper text MUST stop exposing `acknowledged` or `legacy acknowledged` as a valid findings workflow affordance.
- **FR-254-004**: Findings workflow actions and guards MUST authorize and mutate against canonical triage semantics only; the active findings workflow must not require or preserve an acknowledge alias.
- **FR-254-005**: The canonical capability registry, role mappings, and workflow-facing authorization expectations MUST remove `tenant_findings.acknowledge` rather than keeping it as a stale alias.
- **FR-254-006**: Productive code paths and workflow-facing tests MUST stop writing, expecting, or advertising `acknowledged` as a valid current findings workflow status.
- **FR-254-007**: Existing findings flows remain functional and in scope only for regression protection across `new`, `triaged`, `in_progress`, `reopened`, `resolved`, `closed`, and `risk_accepted` outcomes.
- **FR-254-008**: The feature MUST NOT reintroduce repair tooling, backfill semantics, new workflow states, migration shims, fallback readers, or historical compatibility logic to preserve the removed acknowledged branch.
- **FR-254-009**: The feature MUST NOT alter verification-check acknowledgement, onboarding-verification acknowledgement, or other non-finding acknowledgement domains unless a path directly depends on findings status compatibility, in which case that dependency must be removed instead of widening the slice.
- **FR-254-010**: Tenant-owned findings keep existing `workspace_id` plus `tenant_id` ownership anchors; no new persisted alias, auxiliary mapping table, or compatibility truth is introduced.
## 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 |
|---|---|---|---|---|---|---|---|---|---|---|
| Findings resource list/detail | `app/Filament/Resources/FindingResource.php` and related pages | no acknowledge-named action or helper text remains; surviving workflow utilities keep canonical wording | full-row click to finding detail remains canonical | existing canonical findings workflow actions only | existing grouped bulk actions only, with no acknowledged vocabulary | existing empty state remains unchanged | existing detail-header workflow actions keep canonical wording only | `N/A` | yes, unchanged for surviving workflow actions | Remove acknowledged vocabulary from filters, badges, disabled helper text, and action guidance without introducing a replacement action |
| Canonical findings summaries and inbox shells | existing `/admin` pages and widgets using shared findings summary builders | no new header actions; drilldown links only | same-page cards, counters, or preview links remain canonical | none | none | existing empty states remain, but they must not mention acknowledged compatibility | `N/A` | `N/A` | no new audit event | This slice updates counts, previews, and wording only |
### Key Entities *(include if feature involves data)*
- **Canonical finding status**: The current findings lifecycle language used on productive findings surfaces and queries. After this cleanup it consists only of the surviving canonical statuses already present in the findings workflow.
- **Findings-derived summary surface**: Any canonical `/admin` overview, inbox, widget, or preview surface that derives open findings work from the shared finding-status helper rather than from a local list of status strings.
- **Findings capability mapping**: The shared capability and role-mapping truth that determines which tenant members can use the surviving findings workflow actions.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Zero productive findings filters, badges, helper texts, or workflow affordances expose `acknowledged` as a current findings workflow status.
- **SC-002**: Zero supported findings permissions or role mappings expose `tenant_findings.acknowledge` after the cleanup.
- **SC-003**: All in-scope findings-derived summary surfaces and findings register surfaces use the same canonical open-status set during regression validation.
- **SC-004**: Representative regression proof still passes for the surviving findings workflow from `new` through `triaged`, `in_progress`, `resolved`, and `risk_accepted` outcomes without introducing a replacement compatibility branch.
## Dependencies
- The canonical findings model and workflow seams where acknowledged compatibility still survives, including the shared findings status helper, workflow service, filter catalog, and capability registry.
- Existing findings-derived summary builders that currently rely on shared open-status helpers for inbox, health, and preview surfaces.
- Existing findings resource and workflow-facing tests that still preserve or assert acknowledged semantics.
## Assumptions
- The current repo truth treats `triaged` as the canonical operator-facing findings workflow semantics and keeps `acknowledged` only as cleanup debt.
- LEAN-001 still applies because the product remains pre-production; historical or local findings rows do not justify compatibility behavior in this slice.
- Spec 253 already covers the adjacent backfill-runtime-surface cleanup, so this slice should not reopen that work while cleaning status semantics.
- Cross-Tenant Compare and Promotion is already refreshed as Spec 043 and is therefore not the next open preparation target here.
- Verification-check acknowledgement remains a separate domain and must not be pulled into this findings cleanup.
## Risks
- Hidden acknowledged residues may still survive in shared summary builders, status badges, or old test fixtures even after the main findings workflow seam is cleaned.
- Local or stale pre-production data containing acknowledged may surface unexpected failures if implementation removes compatibility before all relevant fixtures and productive write paths are updated together.
- Overbroad cleanup could accidentally touch verification or onboarding acknowledgement semantics, which would violate the intended slice boundary.
## Out of Scope
- Removing or revisiting the already-separated backfill-runtime-surface cleanup slice
- Enforcing creation-time finding invariants or generator hardening beyond what is needed to stop preserving acknowledged compatibility
- Broader findings lifecycle redesign, new workflow states, or new customer-facing workflow surfaces
- Historical data migration, translation helpers, fallback readers, or compatibility-specific test preservation
- Verification-check acknowledgement, onboarding acknowledgement UX, or non-finding acknowledgement domains
- External Support Desk / PSA Handoff or other commercialization workflow work
## Follow-up Candidates
1. `Enforce Creation-Time Finding Invariants` remains the next findings hardening candidate after this semantics cleanup because generator and recurrence guarantees still need explicit protection.
2. `External Support Desk / PSA Handoff` remains an explicit deferred candidate for commercialization flow maturity once the repo names a concrete external desk target.
3. `Cross-Tenant Compare and Promotion v1` remains covered by refreshed Spec 043 and should continue on that track instead of being reopened inside this cleanup slice.

View File

@ -0,0 +1,238 @@
# Tasks: Remove Legacy Acknowledged Finding Status Compatibility
**Input**: Design documents from `/specs/254-remove-acknowledged-compat/`
**Prerequisites**: `plan.md`, `spec.md`, `checklists/requirements.md`
**Tests (TEST-GOV-001)**: REQUIRED (Pest). Keep proof in the targeted `fast-feedback` and `confidence` lanes named in `specs/254-remove-acknowledged-compat/plan.md`, plus the retained `heavy-governance` guards already called out there. Prefer focused new proof in `apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php`, `apps/platform/tests/Unit/Findings/FindingStatusSemanticsTest.php`, `apps/platform/tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php`, and `apps/platform/tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`, then prove summary convergence through the existing downstream customer-health, governance-inbox, baseline, alert, tenant-review, and support-diagnostics tests instead of widening the suite.
**Operations**: This slice does not add or change an `OperationRun` start family, does not add a new audit action ID, and must not introduce migration shims, fallback readers, or repair tooling.
**RBAC**: Preserve current `/admin/t/{tenant}` tenant isolation, deny-as-not-found `404` for non-members or out-of-scope users, and `403` for in-scope capability failures on surviving findings actions. Remove `tenant_findings.acknowledge` from the capability registry and role mappings without widening any unrelated authorization behavior.
**UI / Surface Guardrails**: This is a `review-mandatory` cleanup across native Filament findings surfaces and canonical `/admin` summary or inbox shells. Keep `standard-native-filament` relief for the tenant findings resource and `global-context-shell` proof for workspace summaries, and do not add panels, assets, local status presenters, or replacement workflow affordances.
**Badges / Filters (BADGE-001 / XCUT-001)**: Remove the legacy acknowledged branch through shared findings status seams only. `BadgeCatalog`, `FilterOptionCatalog`, and the existing findings resource or summary builders remain the supported paths; no page-local mapping or one-off status label is allowed.
**Organization**: Tasks are grouped by user story so each slice stays independently verifiable. Recommended delivery order is Phase 1 -> Phase 2 -> `US1` and `US2` in parallel -> `US3` -> final cleanup and validation, because canonical RBAC proof only matters after the workflow and summary surfaces stop carrying acknowledged compatibility.
**Implementation note**: Several downstream consumers already converged automatically once `Finding::openStatusesForQuery()` and the shared RBAC/filter seams were corrected. Where direct edits in the originally listed consumer files proved unnecessary, completion below reflects the shared-helper cleanup plus targeted validation in the existing downstream tests rather than redundant file-local rewrites.
## Test Governance Checklist
- [x] Lane assignment stays `fast-feedback` plus `confidence`, with retained `heavy-governance` guards only in `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` and `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`, and remains the narrowest sufficient proof for the removed compatibility branch.
- [x] New or changed tests stay in focused `Feature` and `Unit` files only; no browser lane or new heavy-governance family is added.
- [x] Shared helpers, factories, fixtures, and context defaults stay cheap by default; acknowledged-specific setup is deleted or localized instead of becoming a new shared default.
- [x] Planned validation commands stay limited to the targeted Sail test commands captured in `specs/254-remove-acknowledged-compat/plan.md` and the final validation phase below.
- [x] The declared surface test profile stays `standard-native-filament` plus `global-context-shell`; no additional surface exception is introduced.
- [x] Any material suite-footprint or residue follow-up resolves inside this feature as `document-in-feature` or `reject-or-split`, not as silent scope drift.
## Phase 1: Setup (Shared Cleanup Anchors)
**Purpose**: Lock the concrete removal inventory, out-of-scope boundaries, and proving commands before implementation starts.
- [x] T001 [P] Verify the productive acknowledged inventory across `apps/platform/app/Models/Finding.php`, `apps/platform/app/Services/Findings/FindingWorkflowService.php`, `apps/platform/app/Policies/FindingPolicy.php`, `apps/platform/app/Support/Auth/Capabilities.php`, `apps/platform/app/Services/Auth/RoleCapabilityMap.php`, `apps/platform/app/Support/Filament/FilterOptionCatalog.php`, `apps/platform/app/Filament/Resources/FindingResource.php`, `apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php`, and `apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php`
- [x] T002 [P] Verify the shared summary, alert, and query-consumer inventory across `apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php`, `apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php`, `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`, `apps/platform/app/Support/Baselines/BaselineCompareStats.php`, `apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php`, `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php`, `apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php`, `apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php`, `apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php`, `apps/platform/app/Services/Baselines/BaselineAutoCloseService.php`, and `apps/platform/app/Services/Findings/FindingAssignmentHygieneService.php`
- [x] T003 [P] Verify the out-of-scope acknowledgement domains stay untouched across `apps/platform/app/Services/Verification/VerificationCheckAcknowledgementService.php`, `apps/platform/app/Filament/Support/VerificationReportViewer.php`, `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantOnboardingWizard.php`, `apps/platform/tests/Feature/Verification/VerificationCheckAcknowledgementTest.php`, and `apps/platform/tests/Feature/Onboarding/OnboardingVerificationV1_5UxTest.php`
- [x] T004 [P] Verify the minimum Sail validation commands and file-scoped coverage expectations in `specs/254-remove-acknowledged-compat/plan.md`, `specs/254-remove-acknowledged-compat/spec.md`, and `specs/254-remove-acknowledged-compat/checklists/requirements.md`
**Checkpoint**: The cleanup boundaries, shared seams, and proving commands are locked before any runtime file is changed.
---
## Phase 2: Foundational (Blocking Proof Surfaces)
**Purpose**: Make the intended regression proof and stale compatibility inventory explicit before removing shared semantics.
**CRITICAL**: No user story work should begin until this phase is complete.
- [x] T005 [P] Create the core status and workflow proof files `apps/platform/tests/Unit/Findings/FindingStatusSemanticsTest.php` and `apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php`
- [x] T006 [P] Create the shared-surface and RBAC proof files `apps/platform/tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php` and `apps/platform/tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`, and prove downstream summary consumers through the existing baseline, workspace health, tenant review, support-diagnostics, and alerts tests that already exercise the shared open-status helper.
- [x] T007 [P] Verify retained guard coverage in `apps/platform/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` and `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php` so findings acknowledged compatibility is treated as removed repo truth rather than tolerated legacy drift
- [x] T008 [P] Audit stale compatibility fixtures and helpers across `apps/platform/database/factories/FindingFactory.php`, `apps/platform/tests/Feature/Findings/Concerns/InteractsWithFindingsWorkflow.php`, `apps/platform/tests/Feature/Models/FindingResolvedTest.php`, `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`, `apps/platform/tests/Unit/Badges/FindingBadgesTest.php`, `apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/OwnerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/OperatorAccessTest.php`, and `apps/platform/tests/Feature/Rbac/RoleMatrix/ReadonlyAccessTest.php`
**Checkpoint**: Proof files, guard expectations, and stale compatibility anchors are explicit and ready for bounded implementation work.
---
## Phase 3: User Story 1 - Use One Canonical Findings Workflow Language (Priority: P1) 🎯 MVP
**Goal**: Remove acknowledged as a productive findings lifecycle concept so tenant findings list, detail, inbox, badges, and filters all speak in one canonical workflow language.
**Independent Test**: Open the tenant findings register, detail, and assignee inbox for a tenant with active findings and verify that statuses, filters, badges, helper text, and workflow affordances use canonical findings vocabulary only.
### Tests for User Story 1
- [x] T009 [P] [US1] Add canonical status-contract and lifecycle assertions in `apps/platform/tests/Unit/Findings/FindingStatusSemanticsTest.php` and `apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php`
- [x] T010 [P] [US1] Add shared filter, badge, and list-surface absence proof in `apps/platform/tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php`, `apps/platform/tests/Unit/Badges/FindingBadgesTest.php`, `apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php`, and `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`
### Implementation for User Story 1
- [x] T011 [US1] Remove productive acknowledged status constants, canonicalization shims, `acknowledge()` helper behavior, and acknowledged factory state usage from `apps/platform/app/Models/Finding.php` and `apps/platform/database/factories/FindingFactory.php`
- [x] T012 [US1] Collapse workflow transitions and policy checks onto canonical triage semantics in `apps/platform/app/Services/Findings/FindingWorkflowService.php` and `apps/platform/app/Policies/FindingPolicy.php`
- [x] T013 [US1] Remove acknowledged filter options, acknowledged detail metadata copy, and acknowledged-specific visibility branches from `apps/platform/app/Support/Filament/FilterOptionCatalog.php`, `apps/platform/app/Filament/Resources/FindingResource.php`, `apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php`, and `apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php`
- [x] T014 [US1] Rewrite stale workflow helper and compatibility assertions in `apps/platform/tests/Feature/Findings/Concerns/InteractsWithFindingsWorkflow.php`, `apps/platform/tests/Feature/Models/FindingResolvedTest.php`, `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`, `apps/platform/tests/Unit/Badges/FindingBadgesTest.php`, and `apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php`
**Checkpoint**: User Story 1 is independently functional and no productive findings surface still advertises acknowledged as current workflow truth.
---
## Phase 4: User Story 2 - Keep Summary Counts and Previews Honest (Priority: P1)
**Goal**: Make every shared summary, preview, alert, and consumer query derive open findings from the same canonical status set used by the findings register.
**Independent Test**: Compare the tenant findings register with the affected `/admin` summary and inbox surfaces for the same tenant or workspace context and verify that counts, previews, drilldowns, and alerts align with the same canonical open-status set.
### Tests for User Story 2
- [x] T015 [P] [US2] Prove shared summary alignment through the existing downstream tests in `apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php` and `apps/platform/tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php`, with the governance-inbox stale-operations expectation recorded separately as an unrelated existing operation-age failure.
- [x] T016 [P] [US2] Add consumer regression coverage for overview, baseline, tenant-review, diagnostics, and alert paths in `apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php`, `apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php`, `apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php`, `apps/platform/tests/Unit/Support/SupportDiagnostics/SupportDiagnosticBundleBuilderTest.php`, and `apps/platform/tests/Feature/Alerts/SlaDueAlertTest.php`
### Implementation for User Story 2
- [x] T017 [US2] Collapse canonical open-status summary builders via the shared `Finding::openStatusesForQuery()` cleanup and validate the unchanged consumers in `apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php`, `apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php`, `apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php`, and `apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php`
- [x] T018 [US2] Remove acknowledged-specific active-count and review-report branches from `apps/platform/app/Support/Baselines/BaselineCompareStats.php` while validating that `apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php` and `apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php` already converged through the shared open-status helper.
- [x] T019 [US2] Collapse alert, generator, and hygiene consumers onto canonical open statuses via the shared helper in `apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php`, `apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php`, `apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php`, `apps/platform/app/Services/Baselines/BaselineAutoCloseService.php`, and `apps/platform/app/Services/Findings/FindingAssignmentHygieneService.php`
- [x] T020 [US2] Rewrite acknowledged-compatibility expectations in `apps/platform/tests/Feature/PermissionPosture/PermissionPostureFindingGeneratorTest.php`, `apps/platform/tests/Feature/EntraAdminRoles/EntraAdminRolesFindingGeneratorTest.php`, and validate the unaffected downstream consumers in `apps/platform/tests/Feature/Alerts/SlaDueAlertTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php`, and `apps/platform/tests/Feature/Findings/FindingsAssignmentHygieneOverviewSignalTest.php`
**Checkpoint**: User Story 2 is independently functional and shared counts, previews, review packs, diagnostics, and alerts all reflect the same canonical findings-open set.
---
## Phase 5: User Story 3 - Keep RBAC Language Canonical (Priority: P2)
**Goal**: Remove the acknowledge capability alias so role guidance, authorization checks, and disabled findings actions all use the surviving canonical findings permission language only.
**Independent Test**: Review capability-driven findings surfaces and role expectations after the cleanup and verify that they reference surviving findings capabilities only while preserving current `404` versus `403` semantics.
### Tests for User Story 3
- [x] T021 [P] [US3] Add positive and negative capability-alias removal coverage in `apps/platform/tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php` and `apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php`
- [x] T022 [P] [US3] Update role-matrix and UI-enforced findings permission proof in `apps/platform/tests/Feature/Rbac/RoleMatrix/OwnerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/OperatorAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/ReadonlyAccessTest.php`, and validate the surviving UI-enforced surface contract in `apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php`
### Implementation for User Story 3
- [x] T023 [US3] Remove `tenant_findings.acknowledge` from `apps/platform/app/Support/Auth/Capabilities.php` and `apps/platform/app/Services/Auth/RoleCapabilityMap.php`
- [x] T024 [US3] Collapse acknowledge-specific authorization branches and findings UI enforcement onto surviving capabilities in `apps/platform/app/Policies/FindingPolicy.php`, `apps/platform/app/Services/Findings/FindingWorkflowService.php`, and `apps/platform/app/Filament/Resources/FindingResource.php`
- [x] T025 [US3] Rewrite stale RBAC and capability-alias expectations in `apps/platform/tests/Feature/Rbac/RoleMatrix/OwnerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/OperatorAccessTest.php`, `apps/platform/tests/Feature/Rbac/RoleMatrix/ReadonlyAccessTest.php`, and `apps/platform/tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`
**Checkpoint**: User Story 3 is independently functional and no supported findings permission path or role expectation still names an acknowledge alias.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Remove remaining stale compatibility residue, keep scope boundaries honest, and run the narrow validation workflow.
- [x] T026 [P] Remove final acknowledged-compatibility residue from findings-only helper and proof surfaces in `apps/platform/tests/Feature/Models/FindingResolvedTest.php`, `apps/platform/tests/Feature/Findings/Concerns/InteractsWithFindingsWorkflow.php`, `apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php`, `apps/platform/tests/Unit/Badges/FindingBadgesTest.php`, and `apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php` after the story-specific rewrites land
- [x] T027 [P] Run a residue search for `STATUS_ACKNOWLEDGED`, `TENANT_FINDINGS_ACKNOWLEDGE`, `legacy acknowledged`, and `acknowledge(` across `apps/platform/app/`, `apps/platform/database/factories/`, and `apps/platform/tests/`, then classify any remaining match as in-scope cleanup, allowed non-finding domain, or `reject-or-split`
- [x] T028 Run formatting for touched PHP files with `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` after the cleanup across `apps/platform/app/`, `apps/platform/database/factories/`, and `apps/platform/tests/`
- [x] T029 [P] Run the focused workflow, filter, and RBAC Sail command `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php tests/Unit/Findings/FindingStatusSemanticsTest.php tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php`
- [x] T030 [P] Run the focused summary and consumer Sail command using the existing downstream proof files `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php tests/Feature/Baselines/BaselineCompareStatsTest.php tests/Feature/Alerts/SlaDueAlertTest.php`, with the governance-inbox stale-operations expectation recorded as an unrelated existing failure outside the acknowledged-status cleanup.
- [x] T031 [P] Run the retained heavy-governance guards `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/NoAdHocStatusBadgesTest.php tests/Feature/Guards/FilamentTableStandardsGuardTest.php`, then verify no out-of-scope verification or onboarding acknowledgement file from `T003` changed without an explicit split decision
- [x] T032 [P] Verify FR-254-010 explicitly by confirming `Finding` keeps `workspace_id` plus `tenant_id` as unchanged ownership anchors and that no file under `apps/platform/database/migrations/` changed and no new persisted compatibility artifact, alias table, fallback reader, or migration-backed truth was introduced while implementing this slice; if ownership-anchor or persistence widening appears, stop and split the work instead of absorbing it here
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: Starts immediately and locks the exact removal inventory, boundaries, and proving commands.
- **Foundational (Phase 2)**: Depends on Setup and blocks all story work until proof files, guard expectations, and stale compatibility anchors are explicit.
- **User Story 1 (Phase 3)**: Depends on Foundational and is part of the MVP delivery.
- **User Story 2 (Phase 4)**: Depends on Foundational and can proceed in parallel with User Story 1 because it targets shared summary, alert, and query consumers behind the same canonical status set.
- **User Story 3 (Phase 5)**: Depends on User Story 1 because RBAC language should be validated only after findings workflow surfaces stop advertising acknowledged semantics; it can overlap with late User Story 2 cleanup once capability surfaces are isolated.
- **Polish (Phase 6)**: Depends on all desired user stories being complete so residue checks, formatting, and focused validation run on the final cleanup shape.
### User Story Dependencies
- **US1**: No dependencies beyond Foundational.
- **US2**: No dependencies beyond Foundational.
- **US3**: Depends on US1 and should validate alongside completed US2 summary cleanup.
### Within Each User Story
- Add or update the story tests first and confirm they fail before cleanup edits are considered complete.
- Remove shared compatibility branches centrally instead of hiding acknowledged semantics on one page or in one helper.
- Do not keep compatibility aliases, fallback readers, data-migration shims, or replacement workflow affordances.
- Keep backfill-runtime-surface removal, creation-time invariants hardening, broader lifecycle redesign, verification acknowledgement cleanup, onboarding acknowledgement cleanup, and support-desk work out of scope.
### Parallel Opportunities
- `T001`, `T002`, `T003`, and `T004` can run in parallel during Setup.
- `T005`, `T006`, `T007`, and `T008` can run in parallel during Foundational work.
- `T009` and `T010` can run in parallel for User Story 1 before `T011`, `T012`, `T013`, and `T014`.
- `T015` and `T016` can run in parallel for User Story 2 before `T017`, `T018`, `T019`, and `T020`.
- User Story 1 and User Story 2 can proceed in parallel after Foundational is complete.
- `T021` and `T022` can run in parallel for User Story 3 before `T023`, `T024`, and `T025`.
- `T029`, `T030`, and `T031` can run in parallel during final validation.
---
## Parallel Example: User Story 1
```bash
# User Story 1 tests in parallel
T009 apps/platform/tests/Unit/Findings/FindingStatusSemanticsTest.php + apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php
T010 apps/platform/tests/Unit/Support/Filament/FindingStatusFilterCatalogTest.php + apps/platform/tests/Unit/Badges/FindingBadgesTest.php + apps/platform/tests/Feature/Support/Badges/FindingBadgeTest.php + apps/platform/tests/Feature/Findings/FindingsIntakeQueueTest.php
# User Story 1 implementation after the tests are in place
T011 apps/platform/app/Models/Finding.php + apps/platform/database/factories/FindingFactory.php
T012 apps/platform/app/Services/Findings/FindingWorkflowService.php + apps/platform/app/Policies/FindingPolicy.php
T013 apps/platform/app/Support/Filament/FilterOptionCatalog.php + apps/platform/app/Filament/Resources/FindingResource.php + apps/platform/app/Filament/Resources/FindingResource/Pages/ListFindings.php + apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php
```
## Parallel Example: User Story 2
```bash
# User Story 2 tests in parallel
T015 apps/platform/tests/Unit/Support/CustomerHealth/WorkspaceHealthSummaryQueryTest.php + apps/platform/tests/Unit/Support/GovernanceInbox/GovernanceInboxSectionBuilderTest.php
T016 apps/platform/tests/Feature/Filament/WorkspaceOverviewSummaryMetricsTest.php + apps/platform/tests/Feature/Filament/WorkspaceOverviewGovernanceAttentionTest.php + apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php + apps/platform/tests/Feature/TenantReview/TenantReviewCanonicalControlReferenceTest.php + apps/platform/tests/Feature/TenantReview/TenantReviewExecutivePackTest.php + apps/platform/tests/Unit/Support/SupportDiagnostics/SupportDiagnosticBundleBuilderTest.php + apps/platform/tests/Feature/Alerts/SlaDueAlertTest.php
# User Story 2 implementation after the tests are in place
T017 apps/platform/app/Support/CustomerHealth/WorkspaceHealthSummaryQuery.php + apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php + apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php + apps/platform/app/Filament/Pages/Findings/MyFindingsInbox.php
T018 apps/platform/app/Support/Baselines/BaselineCompareStats.php + apps/platform/app/Services/TenantReviews/TenantReviewSectionFactory.php + apps/platform/app/Support/SupportDiagnostics/SupportDiagnosticBundleBuilder.php
T019 apps/platform/app/Jobs/Alerts/EvaluateAlertsJob.php + apps/platform/app/Services/PermissionPosture/PermissionPostureFindingGenerator.php + apps/platform/app/Services/EntraAdminRoles/EntraAdminRolesFindingGenerator.php + apps/platform/app/Services/Baselines/BaselineAutoCloseService.php + apps/platform/app/Services/Findings/FindingAssignmentHygieneService.php
```
## Parallel Example: User Story 3
```bash
# User Story 3 tests in parallel
T021 apps/platform/tests/Feature/Auth/RemoveAcknowledgedCapabilityAliasTest.php + apps/platform/tests/Feature/Findings/RemoveAcknowledgedCompatibilityWorkflowTest.php
T022 apps/platform/tests/Feature/Rbac/RoleMatrix/OwnerAccessTest.php + apps/platform/tests/Feature/Rbac/RoleMatrix/ManagerAccessTest.php + apps/platform/tests/Feature/Rbac/RoleMatrix/OperatorAccessTest.php + apps/platform/tests/Feature/Rbac/RoleMatrix/ReadonlyAccessTest.php + apps/platform/tests/Feature/Guards/FilamentTableStandardsGuardTest.php
# User Story 3 implementation after the tests are in place
T023 apps/platform/app/Support/Auth/Capabilities.php + apps/platform/app/Services/Auth/RoleCapabilityMap.php
T024 apps/platform/app/Policies/FindingPolicy.php + apps/platform/app/Services/Findings/FindingWorkflowService.php + apps/platform/app/Filament/Resources/FindingResource.php
```
---
## Implementation Strategy
### MVP First (User Stories 1 and 2)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Complete Phase 4: User Story 2.
5. Run `T028`, `T029`, and `T030` before widening into capability-alias cleanup.
### Incremental Delivery
1. Lock the shared seams, out-of-scope domains, and proving commands.
2. Remove acknowledged from the findings model, workflow, filter, badge, and Filament surfaces.
3. Remove acknowledged from summary builders, review or report helpers, alerts, and other shared query consumers.
4. Remove the stale capability alias and role expectations once findings workflow language is already canonical.
5. Finish with residue searches, formatting, and the focused Sail commands.
### Parallel Team Strategy
1. One contributor can own the findings model, workflow, and Filament cleanup (`US1`) while another owns shared summaries, alerts, review helpers, and query consumers (`US2`) after Phase 2.
2. Once the two P1 stories land, a focused pass can remove the capability alias and RBAC wording (`US3`) without reopening summary or workflow decisions.
3. A final pass can remove stale residue, run Pint, and execute the three focused Sail validation commands.
---
## Notes
- Suggested MVP scope: Phase 1 through Phase 4 only. Canonical workflow cleanup without summary or query-consumer alignment is not sufficient for this feature.
- Explicit non-goals remain: backfill-runtime-surface removal, creation-time invariant hardening, broader lifecycle redesign, verification or onboarding acknowledgement cleanup, new workflow states, migration shims, repair tooling, and support-desk workflows.
- Filament stays on Livewire v4 and no panel/provider or asset strategy changes are needed; `FindingResource` already has a view page, so global-search behavior does not need separate tasking in this slice.
- All tasks above follow the required checklist format with task ID, optional parallel marker, story label where applicable, and concrete file paths.