$budget */ public static function fromArray(array $budget): self { if (! isset($budget['thresholdSeconds'], $budget['baselineSource'], $budget['enforcement'], $budget['lifecycleState'])) { throw new InvalidArgumentException('Budget declarations must define thresholdSeconds, baselineSource, enforcement, and lifecycleState.'); } return new self( thresholdSeconds: (int) $budget['thresholdSeconds'], baselineSource: (string) $budget['baselineSource'], enforcement: (string) $budget['enforcement'], lifecycleState: (string) $budget['lifecycleState'], baselineDeltaTargetPercent: isset($budget['baselineDeltaTargetPercent']) ? (int) $budget['baselineDeltaTargetPercent'] : null, notes: isset($budget['notes']) ? (string) $budget['notes'] : null, reviewCadence: isset($budget['reviewCadence']) ? (string) $budget['reviewCadence'] : null, ); } /** * @return array */ public function evaluate(float $measuredSeconds): array { $budgetStatus = 'within-budget'; if ($measuredSeconds > $this->thresholdSeconds) { $budgetStatus = $this->enforcement === 'warn' ? 'warning' : 'over-budget'; } return array_filter([ 'thresholdSeconds' => $this->thresholdSeconds, 'baselineSource' => $this->baselineSource, 'enforcement' => $this->enforcement, 'lifecycleState' => $this->lifecycleState, 'baselineDeltaTargetPercent' => $this->baselineDeltaTargetPercent, 'measuredSeconds' => round($measuredSeconds, 6), 'budgetStatus' => $budgetStatus, ], static fn (mixed $value): bool => $value !== null); } /** * @param list> $familyBudgets * @param array $durationsByFile * @return list>> */ public static function evaluateFamilyBudgets(array $familyBudgets, array $durationsByFile): array { $evaluations = []; foreach ($familyBudgets as $familyBudget) { $matchedSelectors = []; $measuredSeconds = 0.0; $selectorType = (string) ($familyBudget['selectorType'] ?? 'path'); $selectors = array_values(array_filter( $familyBudget['selectors'] ?? [], static fn (mixed $selector): bool => is_string($selector) && $selector !== '', )); foreach ($durationsByFile as $filePath => $duration) { foreach ($selectors as $selector) { $matches = match ($selectorType) { 'file' => $filePath === $selector, default => str_starts_with($filePath, rtrim($selector, '/')), }; if (! $matches) { continue; } $matchedSelectors[] = $selector; $measuredSeconds += (float) $duration; break; } } $budget = self::fromArray([ 'thresholdSeconds' => (int) $familyBudget['thresholdSeconds'], 'baselineSource' => (string) $familyBudget['baselineSource'], 'enforcement' => (string) $familyBudget['enforcement'], 'lifecycleState' => (string) $familyBudget['lifecycleState'], ]); $evaluations[] = array_merge([ 'familyId' => (string) $familyBudget['familyId'], ], $budget->evaluate($measuredSeconds), [ 'matchedSelectors' => array_values(array_unique($matchedSelectors)), ]); } return $evaluations; } }