$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); } /** * @return list> */ public static function enforcementProfiles(): array { $fastFeedbackBudget = TestLaneManifest::lane('fast-feedback')['budget']; $confidenceBudget = TestLaneManifest::lane('confidence')['budget']; $browserBudget = TestLaneManifest::lane('browser')['budget']; $heavyGovernanceContract = TestLaneManifest::heavyGovernanceBudgetContract(); return [ [ 'policyId' => 'pr-fast-feedback-budget', 'laneId' => 'fast-feedback', 'triggerClass' => 'pull-request', 'thresholdSource' => 'lane-budget', 'baseThresholdSeconds' => (int) $fastFeedbackBudget['thresholdSeconds'], 'varianceAllowanceSeconds' => 15, 'effectiveThresholdSeconds' => (int) $fastFeedbackBudget['thresholdSeconds'] + 15, 'enforcementMode' => 'hard-fail', 'lifecycleState' => (string) $fastFeedbackBudget['lifecycleState'], 'reviewCadence' => 'revisit after two stable CI pull request runs', ], [ 'policyId' => 'main-confidence-budget', 'laneId' => 'confidence', 'triggerClass' => 'mainline-push', 'thresholdSource' => 'lane-budget', 'baseThresholdSeconds' => (int) $confidenceBudget['thresholdSeconds'], 'varianceAllowanceSeconds' => 30, 'effectiveThresholdSeconds' => (int) $confidenceBudget['thresholdSeconds'] + 30, 'enforcementMode' => 'soft-warn', 'lifecycleState' => (string) $confidenceBudget['lifecycleState'], 'reviewCadence' => 'tighten after two stable dev runs', ], [ 'policyId' => 'heavy-governance-manual-budget', 'laneId' => 'heavy-governance', 'triggerClass' => 'manual', 'thresholdSource' => 'governance-contract', 'baseThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']), 'varianceAllowanceSeconds' => 15, 'effectiveThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']) + 15, 'enforcementMode' => 'soft-warn', 'lifecycleState' => (string) $heavyGovernanceContract['lifecycleState'], 'reviewCadence' => 'manual heavy validation must stabilize before schedule enablement', ], [ 'policyId' => 'heavy-governance-scheduled-budget', 'laneId' => 'heavy-governance', 'triggerClass' => 'scheduled', 'thresholdSource' => 'governance-contract', 'baseThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']), 'varianceAllowanceSeconds' => 15, 'effectiveThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']) + 15, 'enforcementMode' => 'trend-only', 'lifecycleState' => (string) $heavyGovernanceContract['lifecycleState'], 'reviewCadence' => 'convert from trend-only only after stable scheduled evidence exists', ], [ 'policyId' => 'browser-manual-budget', 'laneId' => 'browser', 'triggerClass' => 'manual', 'thresholdSource' => 'lane-budget', 'baseThresholdSeconds' => (int) $browserBudget['thresholdSeconds'], 'varianceAllowanceSeconds' => 20, 'effectiveThresholdSeconds' => (int) $browserBudget['thresholdSeconds'] + 20, 'enforcementMode' => 'soft-warn', 'lifecycleState' => (string) $browserBudget['lifecycleState'], 'reviewCadence' => 'tighten after two stable browser validation runs', ], [ 'policyId' => 'browser-scheduled-budget', 'laneId' => 'browser', 'triggerClass' => 'scheduled', 'thresholdSource' => 'lane-budget', 'baseThresholdSeconds' => (int) $browserBudget['thresholdSeconds'], 'varianceAllowanceSeconds' => 20, 'effectiveThresholdSeconds' => (int) $browserBudget['thresholdSeconds'] + 20, 'enforcementMode' => 'trend-only', 'lifecycleState' => (string) $browserBudget['lifecycleState'], 'reviewCadence' => 'convert from trend-only only after stable scheduled evidence exists', ], ]; } /** * @return array */ public static function enforcementProfile(string $laneId, string $triggerClass): array { foreach (self::enforcementProfiles() as $profile) { if ($profile['laneId'] === $laneId && $profile['triggerClass'] === $triggerClass) { return $profile; } } throw new InvalidArgumentException(sprintf('Unknown trigger-aware budget profile for lane [%s] and trigger [%s].', $laneId, $triggerClass)); } /** * @param array $profile * @return array */ public static function evaluateTriggerAwareBudgetProfile(array $profile, float $measuredSeconds): array { foreach (['laneId', 'triggerClass', 'baseThresholdSeconds', 'varianceAllowanceSeconds', 'effectiveThresholdSeconds', 'enforcementMode', 'lifecycleState'] as $requiredKey) { if (! array_key_exists($requiredKey, $profile)) { throw new InvalidArgumentException(sprintf('Trigger-aware budget profiles must define [%s].', $requiredKey)); } } $baseThresholdSeconds = (float) $profile['baseThresholdSeconds']; $effectiveThresholdSeconds = (float) $profile['effectiveThresholdSeconds']; $budgetStatus = 'within-budget'; if ($measuredSeconds > $effectiveThresholdSeconds) { $budgetStatus = 'over-budget'; } elseif ($measuredSeconds > $baseThresholdSeconds) { $budgetStatus = 'warning'; } $blockingStatus = match ((string) $profile['enforcementMode']) { 'hard-fail' => match ($budgetStatus) { 'over-budget' => 'blocking', 'warning' => 'non-blocking-warning', default => 'informational', }, 'soft-warn' => $budgetStatus === 'within-budget' ? 'informational' : 'non-blocking-warning', 'trend-only' => 'informational', default => throw new InvalidArgumentException(sprintf('Unknown enforcement mode [%s].', $profile['enforcementMode'])), }; return array_merge($profile, [ 'measuredSeconds' => round($measuredSeconds, 6), 'budgetStatus' => $budgetStatus, 'blockingStatus' => $blockingStatus, 'primaryFailureClassId' => $budgetStatus === 'within-budget' ? null : 'budget-breach', ]); } /** * @return array */ public static function evaluateLaneForTrigger(string $laneId, string $triggerClass, float $measuredSeconds): array { return self::evaluateTriggerAwareBudgetProfile( self::enforcementProfile($laneId, $triggerClass), $measuredSeconds, ); } /** * @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); } }