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{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 { $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); $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, ]; 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; } 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, ): array { $artifactPaths = self::artifactPaths($laneId, $artifactDirectory); self::ensureDirectory(dirname(TestLaneManifest::absolutePath($artifactPaths['summary']))); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['summary']), self::buildSummaryMarkdown($report), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['budget']), json_encode([ '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, ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); file_put_contents( TestLaneManifest::absolutePath($artifactPaths['report']), json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), ); if (is_string($profileOutput) && trim($profileOutput) !== '') { file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput); } return $artifactPaths; } /** * @return array */ public static function finalizeLane( string $laneId, float $wallClockSeconds, string $capturedOutput = '', ?string $comparisonProfile = null, ): array { $artifactPaths = self::artifactPaths($laneId); $parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId); $report = self::buildReport( laneId: $laneId, wallClockSeconds: $wallClockSeconds, slowestEntries: $parsed['slowestEntries'], durationsByFile: $parsed['durationsByFile'], comparisonProfile: $comparisonProfile, ); self::writeArtifacts($laneId, $report, $capturedOutput); 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 (($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), ]; } private static function ensureDirectory(string $directory): void { if (is_dir($directory)) { return; } mkdir($directory, 0777, true); } }