$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 = in_array($this->enforcement, ['report-only', 'warn'], true) ? '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, 'notes' => $this->notes, 'reviewCadence' => $this->reviewCadence, ], static fn (mixed $value): bool => $value !== null); } /** * @param array $budgetTarget * @return array */ public static function evaluateBudgetTarget(array $budgetTarget, float $measuredSeconds): array { if (! isset($budgetTarget['targetType'], $budgetTarget['targetId'])) { throw new InvalidArgumentException('Budget targets must define targetType and targetId.'); } $evaluation = self::fromArray($budgetTarget)->evaluate($measuredSeconds); return array_merge([ 'budgetId' => (string) ($budgetTarget['budgetId'] ?? sprintf('%s-%s', $budgetTarget['targetType'], $budgetTarget['targetId'])), 'targetType' => (string) $budgetTarget['targetType'], 'targetId' => (string) $budgetTarget['targetId'], ], $evaluation); } /** * @param list> $budgetTargets * @param array $classificationTotals * @param array $familyTotals * @return list> */ public static function evaluateBudgetTargets( array $budgetTargets, float $laneSeconds, array $classificationTotals, array $familyTotals, ): array { $evaluations = []; foreach ($budgetTargets as $budgetTarget) { $targetType = (string) ($budgetTarget['targetType'] ?? ''); $targetId = (string) ($budgetTarget['targetId'] ?? ''); if ($targetType === '' || $targetId === '') { continue; } $measuredSeconds = match ($targetType) { 'lane' => $laneSeconds, 'classification' => (float) ($classificationTotals[$targetId] ?? 0.0), 'family' => (float) ($familyTotals[$targetId] ?? 0.0), default => 0.0, }; $evaluations[] = self::evaluateBudgetTarget($budgetTarget, $measuredSeconds); } return $evaluations; } }