summary_counts) ? $run->summary_counts : [], ); $context = is_array($run->context) ? $run->context : []; $capability = self::capabilityForRun($run, $summaryCounts, $context); return match ($capability) { self::COUNTED => self::countedModel($summaryCounts), self::PHASED => self::phasedModel($run, $context), self::COMPOSITE => self::compositeModel($run, $summaryCounts, $context), self::ACTIVITY => self::indeterminateModel( self::ACTIVITY, (string) $run->status === OperationRunStatus::Queued->value ? 'Waiting for worker.' : 'Progress details pending.', ), default => self::noneModel(), }; } /** * @param array $summaryCounts * @param array $context */ private static function capabilityForRun(OperationRun $run, array $summaryCounts, array $context): string { if (! $run->isCurrentlyActive()) { return self::NONE; } if ((string) $run->status === OperationRunStatus::Queued->value) { return self::ACTIVITY; } if (self::hasPhasedHint($context)) { return self::PHASED; } if (self::hasCompositeHint($summaryCounts, $context)) { return self::COMPOSITE; } if (self::hasCountedHint($summaryCounts)) { return self::COUNTED; } return self::ACTIVITY; } /** * @param array $summaryCounts */ private static function hasCountedHint(array $summaryCounts): bool { $total = $summaryCounts['total'] ?? null; $processed = $summaryCounts['processed'] ?? null; return is_int($total) && $total > 0 && is_int($processed); } /** * @param array $summaryCounts * @return array{ * capability: string, * display: string, * label: string, * processed: int, * total: int, * percent: int * } */ private static function countedModel(array $summaryCounts): array { $total = max(1, (int) ($summaryCounts['total'] ?? 0)); $processed = min(max(0, (int) ($summaryCounts['processed'] ?? 0)), $total); $percent = max(0, min(100, (int) round(($processed / $total) * 100))); return [ 'capability' => self::COUNTED, 'display' => self::COUNTED, 'label' => sprintf('%d / %d processed (%d%%)', $processed, $total, $percent), 'processed' => $processed, 'total' => $total, 'percent' => $percent, ]; } /** * @param array $context * @return array{ * capability: string, * display: string, * label: string, * processed: null, * total: null, * percent: null * } */ private static function phasedModel(OperationRun $run, array $context): array { $phase = self::phaseProgressMetadata($context); if ($phase !== null) { return self::indeterminateModel(self::PHASED, $phase['label']); } return self::indeterminateModel( self::PHASED, self::legacyPhasedLabel($run, $context) ?? 'Phase progress pending.', ); } /** * @param array $summaryCounts * @param array $context * @return array{ * capability: string, * display: string, * label: string, * processed: null, * total: null, * percent: null * } */ private static function compositeModel(OperationRun $run, array $summaryCounts, array $context): array { $label = self::explicitCompositeLabel($context) ?? self::legacyCompositeLabel($run, $summaryCounts, $context) ?? 'Composite progress pending.'; return self::indeterminateModel(self::COMPOSITE, $label); } /** * @return array{ * capability: string, * display: string, * label: string, * processed: null, * total: null, * percent: null * } */ private static function indeterminateModel(string $capability, string $label): array { return [ 'capability' => $capability, 'display' => 'indeterminate', 'label' => $label, 'processed' => null, 'total' => null, 'percent' => null, ]; } /** * @return array{ * capability: string, * display: string, * label: null, * processed: null, * total: null, * percent: null * } */ private static function noneModel(): array { return [ 'capability' => self::NONE, 'display' => self::NONE, 'label' => null, 'processed' => null, 'total' => null, 'percent' => null, ]; } /** * @param array $context */ private static function hasPhasedHint(array $context): bool { if (self::phaseProgressMetadata($context) !== null) { return true; } foreach (['baseline_capture.evidence_capture', 'baseline_compare.evidence_capture'] as $path) { $phaseStats = data_get($context, $path); if (is_array($phaseStats) && self::looksLikePhaseStats($phaseStats)) { return true; } } return false; } /** * @param array $phaseStats */ private static function looksLikePhaseStats(array $phaseStats): bool { return count(array_intersect( array_keys($phaseStats), ['requested', 'succeeded', 'skipped', 'failed', 'throttled'], )) >= 2; } /** * @param array $summaryCounts * @param array $context */ private static function hasCompositeHint(array $summaryCounts, array $context): bool { if (self::explicitCompositeLabel($context) !== null) { return true; } $operationCount = $summaryCounts['operation_count'] ?? null; if (is_int($operationCount) && $operationCount > 1) { return true; } foreach (['child_run_ids', 'operation_run_ids'] as $path) { $runIds = data_get($context, $path); if (! is_array($runIds)) { continue; } $numericIds = array_filter($runIds, static fn (mixed $runId): bool => is_numeric($runId)); if (count($numericIds) > 1) { return true; } } return false; } /** * @param array $context * @return array{key: string, label: string}|null */ private static function phaseProgressMetadata(array $context): ?array { $phase = data_get($context, 'progress.phase'); if (! is_array($phase)) { return null; } $key = self::cleanString($phase['key'] ?? null); if ($key === null || ! in_array($key, ['preparing', 'fetching', 'processing', 'persisting', 'finalizing'], true)) { return null; } $label = self::cleanString($phase['label'] ?? null) ?? self::defaultPhaseLabel($key); return [ 'key' => $key, 'label' => $label, ]; } private static function defaultPhaseLabel(string $key): string { return match ($key) { 'preparing' => 'Preparing work.', 'fetching' => 'Collecting required evidence.', 'processing' => 'Processing current work.', 'persisting' => 'Saving results.', 'finalizing' => 'Finalizing operation.', default => 'Phase progress pending.', }; } /** * @param array $context */ private static function explicitCompositeLabel(array $context): ?string { return self::cleanString(data_get($context, 'progress.composite.label')); } /** * @param array $context */ private static function legacyPhasedLabel(OperationRun $run, array $context): ?string { return match ((string) $run->type) { 'baseline_capture' => is_array(data_get($context, 'baseline_capture.evidence_capture')) ? 'Capturing evidence.' : null, 'baseline_compare' => is_array(data_get($context, 'baseline_compare.evidence_capture')) ? 'Refreshing comparison evidence.' : null, default => null, }; } /** * @param array $summaryCounts * @param array $context */ private static function legacyCompositeLabel(OperationRun $run, array $summaryCounts, array $context): ?string { if ((string) $run->type !== 'tenant.review.compose') { return null; } $operationCount = self::intOrNull($summaryCounts['operation_count'] ?? data_get($context, 'progress.composite.operation_count')); $failedCount = self::intOrNull(data_get($context, 'progress.composite.failed_count')); $partialCount = self::intOrNull(data_get($context, 'progress.composite.partial_count')); $baseLabel = $operationCount !== null && $operationCount > 0 ? sprintf('Review composition is aggregating %d %s.', $operationCount, $operationCount === 1 ? 'operation' : 'operations') : 'Review composition is aggregating related operations.'; $attentionLabel = self::compositeAttentionLabel($failedCount, $partialCount); return $attentionLabel === null ? $baseLabel : sprintf('%s %s', $baseLabel, $attentionLabel); } private static function compositeAttentionLabel(?int $failedCount, ?int $partialCount): ?string { $failedCount = $failedCount !== null && $failedCount > 0 ? $failedCount : null; $partialCount = $partialCount !== null && $partialCount > 0 ? $partialCount : null; if ($failedCount === null && $partialCount === null) { return null; } if ($failedCount !== null && $partialCount !== null) { return sprintf( '%d %s and %d %s currently need review.', $failedCount, $failedCount === 1 ? 'failed operation' : 'failed operations', $partialCount, $partialCount === 1 ? 'partial operation' : 'partial operations', ); } if ($failedCount !== null) { return sprintf( '%d %s currently need review.', $failedCount, $failedCount === 1 ? 'failed operation' : 'failed operations', ); } return sprintf( '%d %s currently need review.', $partialCount, $partialCount === 1 ? 'partial operation' : 'partial operations', ); } private static function cleanString(mixed $value): ?string { if (! is_string($value)) { return null; } $value = trim($value); return $value === '' ? null : $value; } private static function intOrNull(mixed $value): ?int { return is_int($value) ? $value : null; } }