sprintf('%s/%s-latest.junit.xml', $directory, $laneId), 'summary' => sprintf('%s/%s-latest.summary.md', $directory, $laneId), 'budget' => sprintf('%s/%s-latest.budget.json', $directory, $laneId), 'report' => sprintf('%s/%s-latest.report.json', $directory, $laneId), 'profile' => sprintf('%s/%s-latest.profile.txt', $directory, $laneId), ]; } /** * @return array */ public static function artifactPublicationStatus(string $laneId, ?string $artifactDirectory = null): array { $artifactPaths = self::artifactPaths($laneId, $artifactDirectory); $artifactFileMap = self::artifactFileMap($artifactPaths); $artifactContract = TestLaneManifest::artifactPublicationContract($laneId); $publishedArtifacts = []; $missingRequiredArtifacts = []; foreach (array_merge($artifactContract['requiredFiles'], $artifactContract['optionalFiles']) as $artifactFile) { if (! array_key_exists($artifactFile, $artifactFileMap)) { continue; } $relativePath = $artifactFileMap[$artifactFile]; $required = in_array($artifactFile, $artifactContract['requiredFiles'], true); $exists = is_file(TestLaneManifest::absolutePath($relativePath)); $publishedArtifacts[] = [ 'artifactType' => $artifactFile, 'relativePath' => $relativePath, 'required' => $required, 'exists' => $exists, ]; if ($required && ! $exists) { $missingRequiredArtifacts[] = $artifactFile; } } return [ 'contractId' => $artifactContract['contractId'], 'laneId' => $laneId, 'sourceDirectory' => $artifactContract['sourceDirectory'], 'requiredFiles' => $artifactContract['requiredFiles'], 'optionalFiles' => $artifactContract['optionalFiles'], 'publishedArtifacts' => $publishedArtifacts, 'missingRequiredArtifacts' => $missingRequiredArtifacts, 'complete' => $missingRequiredArtifacts === [], 'primaryFailureClassId' => $missingRequiredArtifacts === [] ? null : 'artifact-publication-failure', ]; } /** * @param array|null $artifactPublicationStatus * @param array|null $budgetOutcome */ public static function classifyPrimaryFailure( ?int $exitCode, ?array $artifactPublicationStatus = null, ?array $budgetOutcome = null, bool $entryPointResolved = true, bool $workflowLaneMatched = true, bool $infrastructureFailure = false, ): ?string { if ($infrastructureFailure) { return 'infrastructure-failure'; } if (! $entryPointResolved || ! $workflowLaneMatched) { return 'wrapper-failure'; } if ($exitCode !== null && $exitCode !== 0) { return 'test-failure'; } if (is_array($artifactPublicationStatus) && ($artifactPublicationStatus['complete'] ?? true) !== true) { return 'artifact-publication-failure'; } if (is_array($budgetOutcome) && ($budgetOutcome['budgetStatus'] ?? 'within-budget') !== 'within-budget') { return 'budget-breach'; } return null; } /** * @param array $report * @param array|null $budgetOutcome * @param array|null $artifactPublicationStatus * @return array */ public static function buildCiSummary( array $report, ?int $exitCode = 0, ?array $budgetOutcome = null, ?array $artifactPublicationStatus = null, bool $entryPointResolved = true, bool $workflowLaneMatched = true, bool $infrastructureFailure = false, ): array { $artifactPublicationStatus ??= self::artifactPublicationStatus((string) $report['laneId']); $budgetOutcome ??= is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null; $primaryFailureClassId = self::classifyPrimaryFailure( exitCode: $exitCode, artifactPublicationStatus: $artifactPublicationStatus, budgetOutcome: $budgetOutcome, entryPointResolved: $entryPointResolved, workflowLaneMatched: $workflowLaneMatched, infrastructureFailure: $infrastructureFailure, ); $budgetStatus = (string) ($budgetOutcome['budgetStatus'] ?? $report['budgetStatus'] ?? 'within-budget'); $blockingStatus = match ($primaryFailureClassId) { 'test-failure', 'wrapper-failure', 'artifact-publication-failure', 'infrastructure-failure' => 'blocking', 'budget-breach' => (string) ($budgetOutcome['blockingStatus'] ?? 'non-blocking-warning'), default => 'informational', }; return [ 'runId' => (string) (getenv('GITEA_RUN_ID') ?: getenv('GITHUB_RUN_ID') ?: sprintf('local-%s', $report['laneId'])), 'workflowId' => (string) ($report['ciContext']['workflowId'] ?? sprintf('local-%s', $report['laneId'])), 'laneId' => (string) $report['laneId'], 'testStatus' => $exitCode === 0 ? 'passed' : 'failed', 'artifactStatus' => ($artifactPublicationStatus['complete'] ?? false) ? 'complete' : 'incomplete', 'budgetStatus' => $budgetStatus, 'blockingStatus' => $blockingStatus, 'primaryFailureClassId' => $primaryFailureClassId, 'publishedArtifacts' => $artifactPublicationStatus['publishedArtifacts'], ]; } /** * @return array */ public static function stageArtifacts(string $laneId, string $stagingDirectory, ?string $artifactDirectory = null): array { $artifactPaths = self::artifactPaths($laneId, $artifactDirectory); $artifactFileMap = self::artifactFileMap($artifactPaths); $artifactContract = TestLaneManifest::artifactPublicationContract($laneId); $stagingRoot = self::stagingRootPath($stagingDirectory); self::ensureDirectory($stagingRoot); $stagedArtifacts = []; $missingRequiredArtifacts = []; foreach (array_merge($artifactContract['requiredFiles'], $artifactContract['optionalFiles']) as $artifactFile) { if (! array_key_exists($artifactFile, $artifactFileMap)) { continue; } $required = in_array($artifactFile, $artifactContract['requiredFiles'], true); $sourceRelativePath = $artifactFileMap[$artifactFile]; $sourceAbsolutePath = TestLaneManifest::absolutePath($sourceRelativePath); if (! is_file($sourceAbsolutePath)) { if ($required) { $missingRequiredArtifacts[] = $artifactFile; } continue; } $stagedFileName = self::stagedArtifactName((string) $artifactContract['stagedNamePattern'], $laneId, $artifactFile); $targetAbsolutePath = $stagingRoot.DIRECTORY_SEPARATOR.$stagedFileName; copy($sourceAbsolutePath, $targetAbsolutePath); $stagedArtifacts[] = [ 'artifactType' => $artifactFile, 'relativePath' => str_starts_with($stagingDirectory, DIRECTORY_SEPARATOR) ? $targetAbsolutePath : trim($stagingDirectory, '/').'/'.$stagedFileName, 'required' => $required, ]; } return [ 'laneId' => $laneId, 'stagedArtifacts' => $stagedArtifacts, 'complete' => $missingRequiredArtifacts === [], 'primaryFailureClassId' => $missingRequiredArtifacts === [] ? null : 'artifact-publication-failure', 'missingRequiredArtifacts' => $missingRequiredArtifacts, 'uploadGroupName' => $artifactContract['uploadGroupName'], ]; } /** * @return array{slowestEntries: list>, durationsByFile: array} */ public static function parseJUnit(string $filePath, string $laneId): array { if (! is_file($filePath)) { return [ 'slowestEntries' => [], 'durationsByFile' => [], ]; } $useInternalErrors = libxml_use_internal_errors(true); $xml = simplexml_load_file($filePath); libxml_clear_errors(); libxml_use_internal_errors($useInternalErrors); if (! $xml instanceof SimpleXMLElement) { return [ 'slowestEntries' => [], 'durationsByFile' => [], ]; } $slowestEntries = []; $durationsByFile = []; foreach ($xml->xpath('//testcase') ?: [] as $testcase) { $rawSubject = trim((string) ($testcase['file'] ?? '')); $subject = $rawSubject !== '' ? $rawSubject : trim((string) ($testcase['name'] ?? 'unknown-testcase')); $duration = round((float) ($testcase['time'] ?? 0.0), 6); $normalizedFile = explode('::', $subject)[0]; $slowestEntries[] = [ 'label' => $subject, 'subject' => $subject, 'filePath' => $normalizedFile, 'durationSeconds' => $duration, 'wallClockSeconds' => $duration, 'laneId' => $laneId, ]; $durationsByFile[$normalizedFile] = round(($durationsByFile[$normalizedFile] ?? 0.0) + $duration, 6); } usort($slowestEntries, static fn (array $left, array $right): int => $right['wallClockSeconds'] <=> $left['wallClockSeconds']); return [ 'slowestEntries' => array_slice($slowestEntries, 0, 10), 'durationsByFile' => $durationsByFile, ]; } /** * @param list> $slowestEntries * @param array $durationsByFile * @return array */ public static function buildReport( string $laneId, float $wallClockSeconds, array $slowestEntries, array $durationsByFile, ?string $artifactDirectory = null, ?string $comparisonProfile = null, ?array $ciContext = null, ): array { $lane = TestLaneManifest::lane($laneId); $heavyGovernanceContract = $laneId === 'heavy-governance' ? TestLaneManifest::heavyGovernanceBudgetContract($wallClockSeconds) : null; if (is_array($heavyGovernanceContract)) { $lane['budget']['thresholdSeconds'] = $heavyGovernanceContract['normalizedThresholdSeconds']; $lane['budget']['lifecycleState'] = $heavyGovernanceContract['lifecycleState']; } $laneBudget = TestLaneBudget::fromArray($lane['budget']); $laneBudgetEvaluation = $laneBudget->evaluate($wallClockSeconds); $resolvedCiContext = array_filter($ciContext ?? TestLaneManifest::currentCiContext($laneId), static fn (mixed $value): bool => $value !== null); $ciBudgetEvaluation = null; if (is_string($resolvedCiContext['triggerClass'] ?? null) && $resolvedCiContext['triggerClass'] !== '') { $ciBudgetEvaluation = TestLaneBudget::evaluateLaneForTrigger( $laneId, (string) $resolvedCiContext['triggerClass'], $wallClockSeconds, ); } $artifactPaths = self::artifactPaths($laneId, $artifactDirectory); $artifacts = []; foreach ($lane['artifacts'] as $artifactMode) { $relativePath = match ($artifactMode) { 'summary' => $artifactPaths['summary'], 'junit-xml' => $artifactPaths['junit'], 'profile-top' => $artifactPaths['profile'], 'budget-report' => $artifactPaths['budget'], default => null, }; if (! is_string($relativePath)) { continue; } $artifacts[] = [ 'artifactMode' => $artifactMode, 'relativePath' => $relativePath, 'machineReadable' => in_array($artifactMode, ['junit-xml', 'budget-report'], true), ]; } $attribution = self::buildAttribution($durationsByFile); $relevantBudgetTargets = self::relevantBudgetTargets( $laneId, $attribution['classificationTotals'], $attribution['familyTotals'], ); if (is_array($heavyGovernanceContract)) { $relevantBudgetTargets = array_values(array_map( static function (array $budgetTarget) use ($heavyGovernanceContract): array { if (($budgetTarget['targetType'] ?? null) !== 'lane' || ($budgetTarget['targetId'] ?? null) !== 'heavy-governance') { return $budgetTarget; } $budgetTarget['thresholdSeconds'] = $heavyGovernanceContract['normalizedThresholdSeconds']; $budgetTarget['lifecycleState'] = $heavyGovernanceContract['lifecycleState']; return $budgetTarget; }, $relevantBudgetTargets, )); } $budgetEvaluations = TestLaneBudget::evaluateBudgetTargets( $relevantBudgetTargets, $wallClockSeconds, $attribution['classificationTotals'], $attribution['familyTotals'], ); $enrichedSlowestEntries = array_values(array_map( static function (array $entry) use ($attribution): array { $filePath = (string) ($entry['filePath'] ?? ''); $familyId = $attribution['fileToFamily'][$filePath] ?? null; $classificationId = $attribution['fileToClassification'][$filePath] ?? null; return array_filter(array_merge($entry, [ 'familyId' => $familyId, 'classificationId' => $classificationId, ]), static fn (mixed $value): bool => $value !== null); }, $slowestEntries, )); $heavyGovernanceContext = $laneId === 'heavy-governance' ? self::buildHeavyGovernanceContext( budgetContract: $heavyGovernanceContract ?? TestLaneManifest::heavyGovernanceBudgetContract(), wallClockSeconds: $wallClockSeconds, artifactPaths: [ 'summary' => $artifactPaths['summary'], 'budget' => $artifactPaths['budget'], 'report' => $artifactPaths['report'], ], classificationAttribution: $attribution['classificationAttribution'], familyAttribution: $attribution['familyAttribution'], slowestEntries: $enrichedSlowestEntries, ) : []; $report = [ 'laneId' => $laneId, 'artifactDirectory' => trim($artifactDirectory ?? TestLaneManifest::artifactDirectory(), '/'), 'finishedAt' => gmdate('c'), 'wallClockSeconds' => round($wallClockSeconds, 6), 'budgetThresholdSeconds' => $laneBudget->thresholdSeconds, 'budgetBaselineSource' => $laneBudget->baselineSource, 'budgetEnforcement' => $laneBudget->enforcement, 'budgetLifecycleState' => $laneBudget->lifecycleState, 'budgetStatus' => $laneBudgetEvaluation['budgetStatus'], 'slowestEntries' => $enrichedSlowestEntries, 'classificationAttribution' => $attribution['classificationAttribution'], 'familyAttribution' => $attribution['familyAttribution'], 'budgetEvaluations' => $budgetEvaluations, 'familyBudgetEvaluations' => array_values(array_filter( $budgetEvaluations, static fn (array $evaluation): bool => ($evaluation['targetType'] ?? null) === 'family', )), 'artifacts' => $artifacts, 'artifactPublicationContract' => TestLaneManifest::artifactPublicationContract($laneId), 'knownWorkflowProfiles' => array_values(array_map( static fn (array $workflowProfile): string => (string) $workflowProfile['workflowId'], TestLaneManifest::workflowProfilesForLane($laneId), )), 'failureClasses' => TestLaneManifest::failureClasses(), ]; if ($heavyGovernanceContext !== []) { $report = array_merge($report, $heavyGovernanceContext); } if ($laneBudget->baselineDeltaTargetPercent !== null) { $report['baselineDeltaTargetPercent'] = $laneBudget->baselineDeltaTargetPercent; } $comparison = self::buildSharedFixtureSlimmingComparison($laneId, $wallClockSeconds, $comparisonProfile); if ($comparison !== null) { $report['sharedFixtureSlimmingComparison'] = $comparison; } if ($resolvedCiContext !== []) { $report['ciContext'] = $resolvedCiContext; } if ($ciBudgetEvaluation !== null) { $report['ciBudgetEvaluation'] = $ciBudgetEvaluation; } return $report; } /** * @param array $report * @return array{summary: string, budget: string, report: string, profile: string} */ public static function writeArtifacts( string $laneId, array $report, ?string $profileOutput = null, ?string $artifactDirectory = null, ?int $exitCode = 0, ): array { $artifactPaths = self::artifactPaths($laneId, $artifactDirectory); self::ensureDirectory(dirname(TestLaneManifest::absolutePath($artifactPaths['summary']))); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['summary']), self::buildSummaryMarkdown($report), ); if (is_string($profileOutput) && trim($profileOutput) !== '') { file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput); } file_put_contents( TestLaneManifest::absolutePath($artifactPaths['budget']), json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['report']), json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); $report['artifactPublication'] = self::artifactPublicationStatus($laneId, $artifactDirectory); $report['ciSummary'] = self::buildCiSummary( report: $report, exitCode: $exitCode, budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null, artifactPublicationStatus: $report['artifactPublication'], entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true), workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['summary']), self::buildSummaryMarkdown($report), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['budget']), json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['report']), json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); return $artifactPaths; } /** * @return array */ public static function finalizeLane( string $laneId, float $wallClockSeconds, string $capturedOutput = '', ?string $comparisonProfile = null, ?int $exitCode = 0, ): array { $artifactPaths = self::artifactPaths($laneId); $parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId); $ciContext = TestLaneManifest::currentCiContext($laneId); $report = self::buildReport( laneId: $laneId, wallClockSeconds: $wallClockSeconds, slowestEntries: $parsed['slowestEntries'], durationsByFile: $parsed['durationsByFile'], comparisonProfile: $comparisonProfile, ciContext: $ciContext, ); self::writeArtifacts($laneId, $report, $capturedOutput, exitCode: $exitCode); return $report; } /** * @param array $report */ private static function buildSummaryMarkdown(array $report): string { $lines = [ '# Test Lane Summary', '', sprintf('- Lane: %s', $report['laneId']), sprintf('- Finished: %s', $report['finishedAt']), sprintf('- Wall clock: %.2f seconds', (float) $report['wallClockSeconds']), sprintf('- Budget: %d seconds (%s)', (int) $report['budgetThresholdSeconds'], $report['budgetStatus']), ]; if (isset($report['ciSummary']) && is_array($report['ciSummary'])) { $lines[] = sprintf( '- CI outcome: %s / %s', (string) $report['ciSummary']['testStatus'], (string) $report['ciSummary']['blockingStatus'], ); if (is_string($report['ciSummary']['primaryFailureClassId'] ?? null)) { $lines[] = sprintf('- Primary failure class: %s', (string) $report['ciSummary']['primaryFailureClassId']); } } if (($report['laneId'] ?? null) === 'heavy-governance' && isset($report['budgetContract']) && is_array($report['budgetContract'])) { $lines[] = sprintf( '- Budget contract: %.0f seconds (%s)', (float) $report['budgetContract']['normalizedThresholdSeconds'], (string) ($report['budgetOutcome']['decisionStatus'] ?? $report['budgetContract']['decisionStatus'] ?? 'pending'), ); $lines[] = sprintf( '- Legacy drift signal: %.0f seconds (pre-normalization evidence)', (float) $report['budgetContract']['evaluationThresholdSeconds'], ); if (isset($report['budgetOutcome']['deltaSeconds'], $report['budgetOutcome']['deltaPercent'])) { $lines[] = sprintf( '- Baseline delta: %+0.2f seconds (%+0.2f%%)', (float) $report['budgetOutcome']['deltaSeconds'], (float) $report['budgetOutcome']['deltaPercent'], ); } if (isset($report['inventoryCoverage']) && is_array($report['inventoryCoverage'])) { $lines[] = sprintf( '- Inventory coverage: %.2f%% across %d required families (%s)', (float) $report['inventoryCoverage']['coveredRuntimePercent'], (int) $report['inventoryCoverage']['requiredFamilyCount'], (bool) $report['inventoryCoverage']['meetsInclusionRule'] ? 'meets inclusion rule' : 'missing required families', ); } } if (isset($report['sharedFixtureSlimmingComparison']) && is_array($report['sharedFixtureSlimmingComparison'])) { $comparison = $report['sharedFixtureSlimmingComparison']; $lines[] = sprintf( '- Shared fixture slimming baseline: %.2f seconds (%s, %+0.2f%%)', (float) $comparison['baselineSeconds'], (string) $comparison['status'], (float) $comparison['deltaPercent'], ); } $lines[] = ''; $lines[] = '## Slowest entries'; foreach ($report['slowestEntries'] as $entry) { $label = (string) ($entry['label'] ?? $entry['subject'] ?? 'unknown'); $lines[] = sprintf('- %s (%.2fs)', $label, (float) ($entry['wallClockSeconds'] ?? $entry['durationSeconds'] ?? 0.0)); } if (($report['classificationAttribution'] ?? []) !== []) { $lines[] = ''; $lines[] = '## Classification attribution'; foreach ($report['classificationAttribution'] as $entry) { $lines[] = sprintf('- %s (%.2fs)', $entry['classificationId'], (float) $entry['totalWallClockSeconds']); } } if (($report['familyAttribution'] ?? []) !== []) { $lines[] = ''; $lines[] = '## Family attribution'; foreach ($report['familyAttribution'] as $entry) { $lines[] = sprintf('- %s [%s] (%.2fs)', $entry['familyId'], $entry['classificationId'], (float) $entry['totalWallClockSeconds']); } } return implode(PHP_EOL, $lines).PHP_EOL; } /** * @return array */ private static function buildAttribution(array $durationsByFile): array { $classificationTotals = []; $familyTotals = []; $classificationHotspots = []; $familyHotspots = []; $fileToClassification = []; $fileToFamily = []; foreach ($durationsByFile as $filePath => $duration) { $family = TestLaneManifest::familyForFile($filePath); $mixedResolution = TestLaneManifest::mixedFileResolution($filePath); $classificationId = $mixedResolution['primaryClassificationId'] ?? ($family['classificationId'] ?? null); if (! is_string($classificationId) || $classificationId === '') { continue; } $classificationTotals[$classificationId] = round(($classificationTotals[$classificationId] ?? 0.0) + $duration, 6); $classificationHotspots[$classificationId][] = $filePath; $fileToClassification[$filePath] = $classificationId; if (is_array($family)) { $familyId = (string) $family['familyId']; $familyTotals[$familyId] = round(($familyTotals[$familyId] ?? 0.0) + $duration, 6); $familyHotspots[$familyId]['classificationId'] = $family['classificationId']; $familyHotspots[$familyId]['hotspotFiles'][] = $filePath; $fileToFamily[$filePath] = $familyId; } } $classificationAttribution = array_values(array_map( static fn (string $classificationId, float $total): array => [ 'classificationId' => $classificationId, 'totalWallClockSeconds' => $total, 'hotspotFiles' => array_values(array_unique($classificationHotspots[$classificationId] ?? [])), ], array_keys($classificationTotals), $classificationTotals, )); usort($classificationAttribution, static fn (array $left, array $right): int => $right['totalWallClockSeconds'] <=> $left['totalWallClockSeconds']); $familyAttribution = array_values(array_map( static function (string $familyId, float $total) use ($familyHotspots): array { return [ 'familyId' => $familyId, 'classificationId' => $familyHotspots[$familyId]['classificationId'], 'totalWallClockSeconds' => $total, 'hotspotFiles' => array_values(array_unique($familyHotspots[$familyId]['hotspotFiles'] ?? [])), ]; }, array_keys($familyTotals), $familyTotals, )); usort($familyAttribution, static fn (array $left, array $right): int => $right['totalWallClockSeconds'] <=> $left['totalWallClockSeconds']); return [ 'classificationTotals' => $classificationTotals, 'familyTotals' => $familyTotals, 'classificationAttribution' => $classificationAttribution, 'familyAttribution' => $familyAttribution, 'fileToClassification' => $fileToClassification, 'fileToFamily' => $fileToFamily, ]; } /** * @param array $classificationTotals * @param array $familyTotals * @return list> */ private static function relevantBudgetTargets( string $laneId, array $classificationTotals, array $familyTotals, ): array { return array_values(array_filter( TestLaneManifest::budgetTargets(), static function (array $budgetTarget) use ($laneId, $classificationTotals, $familyTotals): bool { $targetType = (string) ($budgetTarget['targetType'] ?? ''); $targetId = (string) ($budgetTarget['targetId'] ?? ''); return match ($targetType) { 'lane' => $targetId === $laneId, 'classification' => array_key_exists($targetId, $classificationTotals), 'family' => array_key_exists($targetId, $familyTotals), default => false, }; }, )); } /** * @return array|null */ private static function buildSharedFixtureSlimmingComparison( string $laneId, float $wallClockSeconds, ?string $comparisonProfile, ): ?array { if ($comparisonProfile !== 'shared-test-fixture-slimming') { return null; } $baseline = TestLaneManifest::comparisonBaseline($comparisonProfile, $laneId); if (! is_array($baseline)) { return null; } $baselineSeconds = (float) $baseline['wallClockSeconds']; $deltaSeconds = round($wallClockSeconds - $baselineSeconds, 6); $deltaPercent = $baselineSeconds > 0 ? round(($deltaSeconds / $baselineSeconds) * 100, 6) : 0.0; $targetImprovementPercent = (int) ($baseline['targetImprovementPercent'] ?? 10); $maxRegressionPercent = (int) ($baseline['maxRegressionPercent'] ?? 5); $status = 'stable'; if ($deltaPercent <= -$targetImprovementPercent) { $status = 'improved'; } elseif ($deltaPercent > $maxRegressionPercent) { $status = 'regressed'; } return [ 'comparisonProfile' => $comparisonProfile, 'baselineFinishedAt' => (string) $baseline['finishedAt'], 'baselineSeconds' => $baselineSeconds, 'deltaSeconds' => $deltaSeconds, 'deltaPercent' => $deltaPercent, 'targetImprovementPercent' => $targetImprovementPercent, 'maxRegressionPercent' => $maxRegressionPercent, 'status' => $status, ]; } /** * @param list> $classificationAttribution * @param list> $familyAttribution * @param list> $slowestEntries * @param array{summary: string, budget: string, report: string} $artifactPaths * @param array $budgetContract * @return array */ private static function buildHeavyGovernanceContext( array $budgetContract, float $wallClockSeconds, array $artifactPaths, array $classificationAttribution, array $familyAttribution, array $slowestEntries, ): array { $inventory = TestLaneManifest::heavyGovernanceHotspotInventory(); $inventoryCoverage = self::buildHeavyGovernanceInventoryCoverage($familyAttribution, $inventory, $wallClockSeconds); $budgetSnapshots = [ TestLaneManifest::heavyGovernanceBudgetSnapshots()[0], [ 'snapshotId' => 'post-slimming', 'capturedAt' => gmdate('c'), 'wallClockSeconds' => round($wallClockSeconds, 6), 'classificationTotals' => array_map( static fn (array $entry): array => [ 'classificationId' => (string) $entry['classificationId'], 'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6), ], $classificationAttribution, ), 'familyTotals' => array_map( static fn (array $entry): array => [ 'familyId' => (string) $entry['familyId'], 'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6), ], $familyAttribution, ), 'slowestEntries' => array_map( static fn (array $entry): array => [ 'label' => (string) $entry['label'], 'wallClockSeconds' => round((float) ($entry['wallClockSeconds'] ?? 0.0), 6), ], $slowestEntries, ), 'artifactPaths' => $artifactPaths, 'budgetStatus' => (string) TestLaneBudget::evaluateGovernanceContract($budgetContract, $wallClockSeconds)['budgetStatus'], ], ]; $remainingOpenFamilies = array_values(array_map( static fn (array $record): string => $record['familyId'], array_filter( $inventory, static fn (array $record): bool => in_array($record['status'], ['retained', 'follow-up'], true), ), )); $stabilizedFamilies = array_values(array_map( static fn (array $record): string => $record['familyId'], array_filter( $inventory, static fn (array $record): bool => $record['status'] === 'slimmed', ), )); $followUpDebt = array_values(array_map( static fn (array $decision): string => $decision['familyId'], array_filter( TestLaneManifest::heavyGovernanceSlimmingDecisions(), static fn (array $decision): bool => in_array($decision['decisionType'], ['retain', 'follow-up'], true), ), )); $budgetOutcome = TestLaneBudget::buildOutcomeRecord( contract: $budgetContract, baselineSnapshot: $budgetSnapshots[0], currentSnapshot: $budgetSnapshots[1], remainingOpenFamilies: $remainingOpenFamilies, justification: $budgetContract['decisionStatus'] === 'recalibrated' ? sprintf( 'The primary workflow-heavy hotspots were slimmed, but the lane still retains intentional surface-guard depth and the workspace settings residual helper cost, so the authoritative threshold is now %.0fs.', $budgetContract['normalizedThresholdSeconds'], ) : 'The primary workflow-heavy hotspots slimmed enough duplicated work for the heavy-governance lane to recover within 300 seconds.', followUpDebt: $followUpDebt, ); return [ 'budgetContract' => $budgetContract, 'hotspotInventory' => $inventory, 'decompositionRecords' => TestLaneManifest::heavyGovernanceDecompositionRecords(), 'slimmingDecisions' => TestLaneManifest::heavyGovernanceSlimmingDecisions(), 'authorGuidance' => TestLaneManifest::heavyGovernanceAuthorGuidance(), 'inventoryCoverage' => $inventoryCoverage, 'budgetSnapshots' => $budgetSnapshots, 'budgetOutcome' => $budgetOutcome, 'remainingOpenFamilies' => $remainingOpenFamilies, 'stabilizedFamilies' => $stabilizedFamilies, ]; } /** * @param list> $familyAttribution * @param list> $inventory * @return array */ private static function buildHeavyGovernanceInventoryCoverage(array $familyAttribution, array $inventory, float $laneSeconds): array { $requiredFamilyIds = []; $coveredSeconds = 0.0; foreach ($familyAttribution as $entry) { $requiredFamilyIds[] = (string) $entry['familyId']; $coveredSeconds += (float) ($entry['totalWallClockSeconds'] ?? 0.0); if (count($requiredFamilyIds) >= 5 && ($laneSeconds <= 0.0 || ($coveredSeconds / $laneSeconds) >= 0.8)) { break; } } $inventoryFamilyIds = array_values(array_map(static fn (array $entry): string => $entry['familyId'], $inventory)); $coveredFamilyIds = array_values(array_intersect($requiredFamilyIds, $inventoryFamilyIds)); $coveredRuntimeSeconds = array_reduce( $familyAttribution, static function (float $carry, array $entry) use ($coveredFamilyIds): float { if (! in_array((string) $entry['familyId'], $coveredFamilyIds, true)) { return $carry; } return $carry + (float) ($entry['totalWallClockSeconds'] ?? 0.0); }, 0.0, ); $coveredRuntimePercent = $laneSeconds > 0.0 ? round(($coveredRuntimeSeconds / $laneSeconds) * 100, 6) : 0.0; return [ 'requiredFamilyCount' => count($requiredFamilyIds), 'requiredFamilyIds' => $requiredFamilyIds, 'inventoryFamilyCount' => count($inventoryFamilyIds), 'inventoryFamilyIds' => $inventoryFamilyIds, 'coveredFamilyIds' => $coveredFamilyIds, 'coveredRuntimeSeconds' => round($coveredRuntimeSeconds, 6), 'coveredRuntimePercent' => $coveredRuntimePercent, 'meetsInclusionRule' => count($coveredFamilyIds) === count($requiredFamilyIds), 'topHotspots' => array_slice($familyAttribution, 0, 10), ]; } /** * @param array $report * @return array */ private static function budgetPayload(array $report): array { return [ 'laneId' => $report['laneId'], 'artifactDirectory' => $report['artifactDirectory'], 'wallClockSeconds' => $report['wallClockSeconds'], 'budgetThresholdSeconds' => $report['budgetThresholdSeconds'], 'budgetBaselineSource' => $report['budgetBaselineSource'], 'budgetEnforcement' => $report['budgetEnforcement'], 'budgetLifecycleState' => $report['budgetLifecycleState'], 'budgetStatus' => $report['budgetStatus'], 'classificationAttribution' => $report['classificationAttribution'], 'familyAttribution' => $report['familyAttribution'], 'budgetEvaluations' => $report['budgetEvaluations'], 'familyBudgetEvaluations' => $report['familyBudgetEvaluations'], 'budgetContract' => $report['budgetContract'] ?? null, 'inventoryCoverage' => $report['inventoryCoverage'] ?? null, 'budgetSnapshots' => $report['budgetSnapshots'] ?? null, 'budgetOutcome' => $report['budgetOutcome'] ?? null, 'remainingOpenFamilies' => $report['remainingOpenFamilies'] ?? null, 'stabilizedFamilies' => $report['stabilizedFamilies'] ?? null, 'sharedFixtureSlimmingComparison' => $report['sharedFixtureSlimmingComparison'] ?? null, 'ciBudgetEvaluation' => $report['ciBudgetEvaluation'] ?? null, 'artifactPublication' => $report['artifactPublication'] ?? null, 'ciSummary' => $report['ciSummary'] ?? null, ]; } /** * @param array{junit: string, summary: string, budget: string, report: string, profile: string} $artifactPaths * @return array */ private static function artifactFileMap(array $artifactPaths): array { return [ 'junit.xml' => $artifactPaths['junit'], 'summary.md' => $artifactPaths['summary'], 'budget.json' => $artifactPaths['budget'], 'report.json' => $artifactPaths['report'], 'profile.txt' => $artifactPaths['profile'], ]; } private static function stagedArtifactName(string $pattern, string $laneId, string $artifactFile): string { return str_replace( ['{laneId}', '{artifactFile}'], [$laneId, $artifactFile], $pattern, ); } private static function stagingRootPath(string $stagingDirectory): string { if (str_starts_with($stagingDirectory, DIRECTORY_SEPARATOR)) { return $stagingDirectory; } return self::repositoryRoot().DIRECTORY_SEPARATOR.ltrim($stagingDirectory, '/'); } private static function repositoryRoot(): string { return TestLaneManifest::repoRoot(); } private static function ensureDirectory(string $directory): void { if (is_dir($directory)) { return; } mkdir($directory, 0777, true); } }