## Summary - introduce a shared operator outcome taxonomy with semantic axes, severity bands, and next-action policy - apply the taxonomy to operations, evidence/review completeness, baseline semantics, and restore semantics - harden badge rendering, tenant-safe filtering/search behavior, and operator-facing summary/notification wording - add the spec kit artifacts, reference documentation, and regression coverage for diagnostic-vs-primary state handling ## Testing - focused Pest coverage for taxonomy registry and badge guardrails - operations presentation and notification tests - evidence, baseline, restore, and tenant-scope regression tests ## Notes - Livewire v4.0+ compliance is preserved in the existing Filament v5 stack - panel provider registration remains unchanged in bootstrap/providers.php - no new globally searchable resource was added; adopted resources remain tenant-safe and out of global search where required - no new destructive action family was introduced; existing actions keep their current authorization and confirmation behavior - no new frontend asset strategy was introduced; existing deploy flow with filament:assets remains unchanged Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #186
647 lines
27 KiB
PHP
647 lines
27 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Badges;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
final class OperatorOutcomeTaxonomy
|
|
{
|
|
/**
|
|
* @var array<string, array<string, array{
|
|
* axis: string,
|
|
* label: string,
|
|
* color: string,
|
|
* classification: string,
|
|
* next_action_policy: string,
|
|
* legacy_aliases: list<string>,
|
|
* diagnostic_label?: string|null,
|
|
* notes: string
|
|
* }>>
|
|
*/
|
|
private const ENTRIES = [
|
|
'operation_run_status' => [
|
|
'queued' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Queued for execution',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Queued'],
|
|
'notes' => 'Execution is waiting for a worker to start the run.',
|
|
],
|
|
'running' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'In progress',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Running'],
|
|
'notes' => 'Execution is currently running.',
|
|
],
|
|
'completed' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Run finished',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Completed'],
|
|
'notes' => 'Execution has reached a terminal state and the outcome badge carries the primary meaning.',
|
|
],
|
|
],
|
|
'operation_run_outcome' => [
|
|
'pending' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Awaiting result',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Pending'],
|
|
'notes' => 'Execution has not produced a terminal outcome yet.',
|
|
],
|
|
'succeeded' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Completed successfully',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Succeeded'],
|
|
'notes' => 'The run finished without operator follow-up.',
|
|
],
|
|
'partially_succeeded' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Completed with follow-up',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Partially succeeded', 'Partial'],
|
|
'notes' => 'The run finished but needs operator review or cleanup.',
|
|
],
|
|
'blocked' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Blocked by prerequisite',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Blocked'],
|
|
'notes' => 'Execution could not start or continue until a prerequisite is fixed.',
|
|
],
|
|
'failed' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Execution failed',
|
|
'color' => 'danger',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Failed'],
|
|
'notes' => 'Execution ended unsuccessfully and needs operator attention.',
|
|
],
|
|
'cancelled' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Cancelled',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Cancelled'],
|
|
'notes' => 'Execution was intentionally stopped.',
|
|
],
|
|
],
|
|
'evidence_completeness' => [
|
|
'complete' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Coverage ready',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Complete'],
|
|
'notes' => 'Required evidence is present.',
|
|
],
|
|
'partial' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Coverage incomplete',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Partial'],
|
|
'notes' => 'Some required evidence dimensions are still missing.',
|
|
],
|
|
'missing' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Not collected yet',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Missing'],
|
|
'notes' => 'No evidence has been captured for this slice yet. This is not a failure by itself.',
|
|
],
|
|
'stale' => [
|
|
'axis' => 'data_freshness',
|
|
'label' => 'Refresh recommended',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Stale'],
|
|
'notes' => 'Evidence exists but is old enough that the operator should refresh it before relying on it.',
|
|
],
|
|
],
|
|
'tenant_review_completeness' => [
|
|
'complete' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Review inputs ready',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Complete'],
|
|
'notes' => 'The review has the evidence inputs it needs.',
|
|
],
|
|
'partial' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Review inputs incomplete',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Partial'],
|
|
'notes' => 'Some review sections still need inputs.',
|
|
],
|
|
'missing' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Review input pending',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Missing'],
|
|
'notes' => 'The review has not been anchored to usable evidence yet.',
|
|
],
|
|
'stale' => [
|
|
'axis' => 'data_freshness',
|
|
'label' => 'Refresh review inputs',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Stale'],
|
|
'notes' => 'The review input exists but should be refreshed before stakeholder use.',
|
|
],
|
|
],
|
|
'baseline_snapshot_fidelity' => [
|
|
'full' => [
|
|
'axis' => 'evidence_depth',
|
|
'label' => 'Detailed evidence',
|
|
'color' => 'success',
|
|
'classification' => 'diagnostic',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Full'],
|
|
'notes' => 'Full structured evidence detail is available.',
|
|
],
|
|
'partial' => [
|
|
'axis' => 'evidence_depth',
|
|
'label' => 'Mixed evidence detail',
|
|
'color' => 'info',
|
|
'classification' => 'diagnostic',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Partial'],
|
|
'notes' => 'Some items have full detail while others are metadata-only.',
|
|
],
|
|
'reference_only' => [
|
|
'axis' => 'evidence_depth',
|
|
'label' => 'Metadata only',
|
|
'color' => 'info',
|
|
'classification' => 'diagnostic',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Reference only'],
|
|
'notes' => 'Only reference metadata is available for this capture.',
|
|
],
|
|
'unsupported' => [
|
|
'axis' => 'product_support_maturity',
|
|
'label' => 'Support limited',
|
|
'color' => 'gray',
|
|
'classification' => 'diagnostic',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Unsupported'],
|
|
'diagnostic_label' => 'Fallback renderer',
|
|
'notes' => 'The renderer fell back to a lower-fidelity representation. This is diagnostic context, not a governance gap.',
|
|
],
|
|
],
|
|
'baseline_snapshot_gap_status' => [
|
|
'clear' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'No follow-up needed',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['No gaps'],
|
|
'notes' => 'The captured group does not contain unresolved coverage gaps.',
|
|
],
|
|
'gaps_present' => [
|
|
'axis' => 'data_coverage',
|
|
'label' => 'Coverage gaps need review',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Gaps present'],
|
|
'notes' => 'The captured group has unresolved gaps that should be reviewed.',
|
|
],
|
|
],
|
|
'restore_run_status' => [
|
|
'draft' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Draft',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Draft'],
|
|
'notes' => 'The restore run has not been prepared yet.',
|
|
],
|
|
'scoped' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Scope selected',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Scoped'],
|
|
'notes' => 'Items were selected for restore.',
|
|
],
|
|
'checked' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Checks complete',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Checked'],
|
|
'notes' => 'Safety checks were completed for this run.',
|
|
],
|
|
'previewed' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Preview ready',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Previewed'],
|
|
'notes' => 'A dry-run preview is available for review.',
|
|
],
|
|
'pending' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Pending execution',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Pending'],
|
|
'notes' => 'Execution has not been queued yet.',
|
|
],
|
|
'queued' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Queued for execution',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Queued'],
|
|
'notes' => 'Execution is queued and waiting for a worker.',
|
|
],
|
|
'running' => [
|
|
'axis' => 'execution_lifecycle',
|
|
'label' => 'Applying restore',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Running'],
|
|
'notes' => 'Execution is currently applying restore work.',
|
|
],
|
|
'completed' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Applied successfully',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Completed'],
|
|
'notes' => 'The restore run finished successfully.',
|
|
],
|
|
'partial' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Applied with follow-up',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Partial'],
|
|
'notes' => 'The restore run finished but needs follow-up on a subset of items.',
|
|
],
|
|
'failed' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Restore failed',
|
|
'color' => 'danger',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Failed'],
|
|
'notes' => 'The restore run did not complete successfully.',
|
|
],
|
|
'cancelled' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Cancelled',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Cancelled'],
|
|
'notes' => 'Execution was intentionally cancelled.',
|
|
],
|
|
'aborted' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Stopped early',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Aborted'],
|
|
'notes' => 'Execution stopped before the normal terminal path completed.',
|
|
],
|
|
'completed_with_errors' => [
|
|
'axis' => 'execution_outcome',
|
|
'label' => 'Applied with follow-up',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Completed with errors'],
|
|
'notes' => 'Execution completed but still needs follow-up on failed items.',
|
|
],
|
|
],
|
|
'restore_result_status' => [
|
|
'applied' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Applied',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Applied'],
|
|
'notes' => 'The item was applied successfully.',
|
|
],
|
|
'dry_run' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Preview only',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Dry run'],
|
|
'notes' => 'The item was only simulated and not applied.',
|
|
],
|
|
'mapped' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Mapped to existing item',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Mapped'],
|
|
'notes' => 'The source item mapped to an existing target.',
|
|
],
|
|
'skipped' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Not applied',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Skipped'],
|
|
'notes' => 'The item was intentionally not applied.',
|
|
],
|
|
'partial' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Partially applied',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Partial'],
|
|
'notes' => 'The item only applied in part and needs review.',
|
|
],
|
|
'manual_required' => [
|
|
'axis' => 'operator_actionability',
|
|
'label' => 'Manual follow-up needed',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Manual required'],
|
|
'notes' => 'The operator must handle this item manually.',
|
|
],
|
|
'failed' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Apply failed',
|
|
'color' => 'danger',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Failed'],
|
|
'notes' => 'The item failed to apply.',
|
|
],
|
|
],
|
|
'restore_preview_decision' => [
|
|
'created' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Will create',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Created'],
|
|
'notes' => 'The preview plans to create a new target item.',
|
|
],
|
|
'created_copy' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Will create copy',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Created copy'],
|
|
'notes' => 'The preview plans to create a copy and should be reviewed before execution.',
|
|
],
|
|
'mapped_existing' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Will map existing',
|
|
'color' => 'info',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Mapped existing'],
|
|
'notes' => 'The preview plans to map this item to an existing target.',
|
|
],
|
|
'skipped' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Will skip',
|
|
'color' => 'gray',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Skipped'],
|
|
'notes' => 'The preview plans to skip this item.',
|
|
],
|
|
'failed' => [
|
|
'axis' => 'item_result',
|
|
'label' => 'Cannot apply',
|
|
'color' => 'danger',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Failed'],
|
|
'notes' => 'The preview could not produce a viable action for this item.',
|
|
],
|
|
],
|
|
'restore_check_severity' => [
|
|
'blocking' => [
|
|
'axis' => 'operator_actionability',
|
|
'label' => 'Fix before running',
|
|
'color' => 'danger',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'required',
|
|
'legacy_aliases' => ['Blocking'],
|
|
'notes' => 'Execution should not proceed until this check is fixed.',
|
|
],
|
|
'warning' => [
|
|
'axis' => 'operator_actionability',
|
|
'label' => 'Review before running',
|
|
'color' => 'warning',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'optional',
|
|
'legacy_aliases' => ['Warning'],
|
|
'notes' => 'Execution may proceed, but the operator should review the warning first.',
|
|
],
|
|
'safe' => [
|
|
'axis' => 'operator_actionability',
|
|
'label' => 'Ready to continue',
|
|
'color' => 'success',
|
|
'classification' => 'primary',
|
|
'next_action_policy' => 'none',
|
|
'legacy_aliases' => ['Safe'],
|
|
'notes' => 'No blocking issue was found for this check.',
|
|
],
|
|
],
|
|
];
|
|
|
|
/**
|
|
* @return array{
|
|
* axis: OperatorSemanticAxis,
|
|
* label: string,
|
|
* color: string,
|
|
* classification: OperatorStateClassification,
|
|
* next_action_policy: OperatorNextActionPolicy,
|
|
* legacy_aliases: list<string>,
|
|
* diagnostic_label: ?string,
|
|
* notes: string
|
|
* }|null
|
|
*/
|
|
public static function entry(BadgeDomain $domain, mixed $value): ?array
|
|
{
|
|
$state = BadgeCatalog::normalizeState($value);
|
|
|
|
if ($state === null) {
|
|
return null;
|
|
}
|
|
|
|
if ($domain === BadgeDomain::OperationRunOutcome && $state === 'operation.blocked') {
|
|
$state = 'blocked';
|
|
}
|
|
|
|
$entry = self::ENTRIES[$domain->value][$state] ?? null;
|
|
|
|
if (! is_array($entry)) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'axis' => self::axisFrom($entry['axis']),
|
|
'label' => $entry['label'],
|
|
'color' => $entry['color'],
|
|
'classification' => self::classificationFrom($entry['classification']),
|
|
'next_action_policy' => self::nextActionPolicyFrom($entry['next_action_policy']),
|
|
'legacy_aliases' => $entry['legacy_aliases'],
|
|
'diagnostic_label' => $entry['diagnostic_label'] ?? null,
|
|
'notes' => $entry['notes'],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, array<string, array{
|
|
* axis: OperatorSemanticAxis,
|
|
* label: string,
|
|
* color: string,
|
|
* classification: OperatorStateClassification,
|
|
* next_action_policy: OperatorNextActionPolicy,
|
|
* legacy_aliases: list<string>,
|
|
* diagnostic_label: ?string,
|
|
* notes: string
|
|
* }>>
|
|
*/
|
|
public static function all(): array
|
|
{
|
|
$entries = [];
|
|
|
|
foreach (self::ENTRIES as $domain => $mappings) {
|
|
foreach ($mappings as $state => $entry) {
|
|
$entries[$domain][$state] = [
|
|
'axis' => self::axisFrom($entry['axis']),
|
|
'label' => $entry['label'],
|
|
'color' => $entry['color'],
|
|
'classification' => self::classificationFrom($entry['classification']),
|
|
'next_action_policy' => self::nextActionPolicyFrom($entry['next_action_policy']),
|
|
'legacy_aliases' => $entry['legacy_aliases'],
|
|
'diagnostic_label' => $entry['diagnostic_label'] ?? null,
|
|
'notes' => $entry['notes'],
|
|
];
|
|
}
|
|
}
|
|
|
|
return $entries;
|
|
}
|
|
|
|
/**
|
|
* @return list<array{name: string, domain: BadgeDomain, raw_value: string}>
|
|
*/
|
|
public static function curatedExamples(): array
|
|
{
|
|
return [
|
|
['name' => 'Operation blocked by missing prerequisite', 'domain' => BadgeDomain::OperationRunOutcome, 'raw_value' => 'blocked'],
|
|
['name' => 'Operation completed with follow-up', 'domain' => BadgeDomain::OperationRunOutcome, 'raw_value' => 'partially_succeeded'],
|
|
['name' => 'Operation completed successfully', 'domain' => BadgeDomain::OperationRunOutcome, 'raw_value' => 'succeeded'],
|
|
['name' => 'Evidence not collected yet', 'domain' => BadgeDomain::EvidenceCompleteness, 'raw_value' => 'missing'],
|
|
['name' => 'Evidence refresh recommended', 'domain' => BadgeDomain::EvidenceCompleteness, 'raw_value' => 'stale'],
|
|
['name' => 'Review input pending', 'domain' => BadgeDomain::TenantReviewCompleteness, 'raw_value' => 'missing'],
|
|
['name' => 'Mixed evidence detail stays diagnostic', 'domain' => BadgeDomain::BaselineSnapshotFidelity, 'raw_value' => 'partial'],
|
|
['name' => 'Support limited stays diagnostic', 'domain' => BadgeDomain::BaselineSnapshotFidelity, 'raw_value' => 'unsupported'],
|
|
['name' => 'Coverage gaps need review', 'domain' => BadgeDomain::BaselineSnapshotGapStatus, 'raw_value' => 'gaps_present'],
|
|
['name' => 'Restore preview blocked by a check', 'domain' => BadgeDomain::RestoreCheckSeverity, 'raw_value' => 'blocking'],
|
|
['name' => 'Restore run applied with follow-up', 'domain' => BadgeDomain::RestoreRunStatus, 'raw_value' => 'completed_with_errors'],
|
|
['name' => 'Restore item requires manual follow-up', 'domain' => BadgeDomain::RestoreResultStatus, 'raw_value' => 'manual_required'],
|
|
];
|
|
}
|
|
|
|
public static function spec(
|
|
BadgeDomain $domain,
|
|
mixed $value,
|
|
?string $icon = null,
|
|
?string $iconColor = null,
|
|
): ?BadgeSpec {
|
|
$entry = self::entry($domain, $value);
|
|
|
|
if ($entry === null) {
|
|
return null;
|
|
}
|
|
|
|
return new BadgeSpec(
|
|
label: $entry['label'],
|
|
color: $entry['color'],
|
|
icon: $icon,
|
|
iconColor: $iconColor,
|
|
semanticAxis: $entry['axis'],
|
|
classification: $entry['classification'],
|
|
nextActionPolicy: $entry['next_action_policy'],
|
|
diagnosticLabel: $entry['diagnostic_label'],
|
|
legacyAliases: $entry['legacy_aliases'],
|
|
notes: $entry['notes'],
|
|
);
|
|
}
|
|
|
|
private static function axisFrom(string $value): OperatorSemanticAxis
|
|
{
|
|
return OperatorSemanticAxis::tryFrom($value)
|
|
?? throw new InvalidArgumentException("Unknown operator semantic axis [{$value}].");
|
|
}
|
|
|
|
private static function classificationFrom(string $value): OperatorStateClassification
|
|
{
|
|
return OperatorStateClassification::tryFrom($value)
|
|
?? throw new InvalidArgumentException("Unknown operator state classification [{$value}].");
|
|
}
|
|
|
|
private static function nextActionPolicyFrom(string $value): OperatorNextActionPolicy
|
|
{
|
|
return OperatorNextActionPolicy::tryFrom($value)
|
|
?? throw new InvalidArgumentException("Unknown operator next-action policy [{$value}].");
|
|
}
|
|
}
|