TenantAtlas/app/Support/Baselines/BaselineScope.php
ahmido a30be84084 Baseline governance UX polish + view Infolist (#123)
Summary:
- Baseline Compare landing: enterprise UI (stats grid, critical drift banner, better actions), navigation grouping under Governance, and Action Surface Contract declaration.
- Baseline Profile view page: switches from disabled form fields to proper Infolist entries for a clean read-only view.
- Fixes tenant name column usages (`display_name` → `name`) in baseline assignment flows.
- Dashboard: improved baseline governance widget with severity breakdown + last compared.

Notes:
- Filament v5 / Livewire v4 compatible.
- Destructive actions remain confirmed (`->requiresConfirmation()`).

Tests:
- `vendor/bin/sail artisan test --compact tests/Feature/Baselines`
- `vendor/bin/sail artisan test --compact tests/Feature/Guards/ActionSurfaceContractTest.php`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #123
2026-02-19 23:56:09 +00:00

114 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\Baselines;
/**
* Value object for baseline scope resolution.
*
* A scope defines which policy types are included in a baseline profile.
* An empty policy_types array means "all types" (no filter).
*/
final class BaselineScope
{
/**
* @param array<string> $policyTypes
*/
public function __construct(
public readonly array $policyTypes = [],
) {}
/**
* Create from the scope_jsonb column value.
*
* @param array<string, mixed>|null $scopeJsonb
*/
public static function fromJsonb(?array $scopeJsonb): self
{
if ($scopeJsonb === null) {
return new self;
}
$policyTypes = $scopeJsonb['policy_types'] ?? [];
return new self(
policyTypes: is_array($policyTypes) ? array_values(array_filter($policyTypes, 'is_string')) : [],
);
}
/**
* Normalize the effective scope by intersecting profile scope with an optional override.
*
* Override can only narrow the profile scope (subset enforcement).
* If the profile scope is empty (all types), the override becomes the effective scope.
* If the override is empty or null, the profile scope is used as-is.
*/
public static function effective(self $profileScope, ?self $overrideScope): self
{
if ($overrideScope === null || $overrideScope->isEmpty()) {
return $profileScope;
}
if ($profileScope->isEmpty()) {
return $overrideScope;
}
$intersected = array_values(array_intersect($profileScope->policyTypes, $overrideScope->policyTypes));
return new self(policyTypes: $intersected);
}
/**
* An empty scope means "all types".
*/
public function isEmpty(): bool
{
return $this->policyTypes === [];
}
/**
* Check if a policy type is included in this scope.
*/
public function includes(string $policyType): bool
{
if ($this->isEmpty()) {
return true;
}
return in_array($policyType, $this->policyTypes, true);
}
/**
* Validate that override is a subset of the profile scope.
*/
public static function isValidOverride(self $profileScope, self $overrideScope): bool
{
if ($overrideScope->isEmpty()) {
return true;
}
if ($profileScope->isEmpty()) {
return true;
}
foreach ($overrideScope->policyTypes as $type) {
if (! in_array($type, $profileScope->policyTypes, true)) {
return false;
}
}
return true;
}
/**
* @return array<string, mixed>
*/
public function toJsonb(): array
{
return [
'policy_types' => $this->policyTypes,
];
}
}