$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; } /** * @param array $contract * @return array */ public static function evaluateGovernanceContract(array $contract, float $measuredSeconds): array { foreach (['laneId', 'summaryThresholdSeconds', 'evaluationThresholdSeconds', 'normalizedThresholdSeconds'] as $requiredKey) { if (! array_key_exists($requiredKey, $contract)) { throw new InvalidArgumentException(sprintf('Governance contracts must define [%s].', $requiredKey)); } } $normalizedThresholdSeconds = (float) $contract['normalizedThresholdSeconds']; if ($normalizedThresholdSeconds <= 0) { throw new InvalidArgumentException('Governance contracts must define a positive normalizedThresholdSeconds value.'); } $enforcementLevel = (string) ($contract['enforcementLevel'] ?? 'warn'); $budgetStatus = 'within-budget'; if ($measuredSeconds > $normalizedThresholdSeconds) { $budgetStatus = in_array($enforcementLevel, ['report-only', 'warn'], true) ? 'warning' : 'over-budget'; } return array_filter([ 'laneId' => (string) $contract['laneId'], 'summaryThresholdSeconds' => (float) $contract['summaryThresholdSeconds'], 'evaluationThresholdSeconds' => (float) $contract['evaluationThresholdSeconds'], 'normalizedThresholdSeconds' => $normalizedThresholdSeconds, 'baselineSource' => (string) ($contract['baselineSource'] ?? 'measured-lane'), 'enforcementLevel' => $enforcementLevel, 'lifecycleState' => (string) ($contract['lifecycleState'] ?? 'draft'), 'decisionStatus' => (string) ($contract['decisionStatus'] ?? 'pending'), 'measuredSeconds' => round($measuredSeconds, 6), 'budgetStatus' => $budgetStatus, 'reconciliationRationale' => isset($contract['reconciliationRationale']) ? (string) $contract['reconciliationRationale'] : null, ], static fn (mixed $value): bool => $value !== null); } /** * @param array $baselineSnapshot * @param array $currentSnapshot * @return array */ public static function compareSnapshots(array $baselineSnapshot, array $currentSnapshot): array { $baselineSeconds = round((float) ($baselineSnapshot['wallClockSeconds'] ?? 0.0), 6); $currentSeconds = round((float) ($currentSnapshot['wallClockSeconds'] ?? 0.0), 6); $deltaSeconds = round($currentSeconds - $baselineSeconds, 6); $deltaPercent = $baselineSeconds > 0.0 ? round(($deltaSeconds / $baselineSeconds) * 100, 6) : 0.0; return [ 'baselineSeconds' => $baselineSeconds, 'currentSeconds' => $currentSeconds, 'deltaSeconds' => $deltaSeconds, 'deltaPercent' => $deltaPercent, ]; } /** * @param array $contract * @param array $baselineSnapshot * @param array $currentSnapshot * @param list $remainingOpenFamilies * @param list $followUpDebt * @return array */ public static function buildOutcomeRecord( array $contract, array $baselineSnapshot, array $currentSnapshot, array $remainingOpenFamilies, string $justification, array $followUpDebt = [], ): array { $comparison = self::compareSnapshots($baselineSnapshot, $currentSnapshot); return array_filter([ 'outcomeId' => sprintf('%s-final-outcome', (string) ($contract['laneId'] ?? 'heavy-governance')), 'decisionStatus' => (string) ($contract['decisionStatus'] ?? 'pending'), 'finalThresholdSeconds' => round((float) ($contract['normalizedThresholdSeconds'] ?? 0.0), 6), 'finalMeasuredSeconds' => $comparison['currentSeconds'], 'deltaSeconds' => $comparison['deltaSeconds'], 'deltaPercent' => $comparison['deltaPercent'], 'remainingOpenFamilies' => array_values($remainingOpenFamilies), 'justification' => $justification, 'followUpDebt' => $followUpDebt !== [] ? array_values($followUpDebt) : null, ], static fn (mixed $value): bool => $value !== null); } }