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::indeterminateModel(self::PHASED, 'Phase progress pending.'), self::COMPOSITE => self::indeterminateModel(self::COMPOSITE, 'Composite progress pending.'), 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, ]; } /** * @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 { 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 { $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; } }