Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m11s
Added UiBloatRegressionGuardTest to enforce known UI bloat and customer/auditor safety regression patterns across configured runtime UI source paths as defined in Spec 375. Registered the test in Pest.php and added to TestLaneManifest.
3424 lines
154 KiB
PHP
3424 lines
154 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Support;
|
|
|
|
use InvalidArgumentException;
|
|
use RecursiveDirectoryIterator;
|
|
use RecursiveIteratorIterator;
|
|
use SplFileInfo;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
final class TestLaneManifest
|
|
{
|
|
private const ARTIFACT_DIRECTORY = 'storage/logs/test-lanes';
|
|
|
|
private const MAINLINE_BRANCH = 'dev';
|
|
|
|
private const CI_RUNNER_LABEL = 'ubuntu-latest';
|
|
|
|
private const FULL_SUITE_BASELINE_SECONDS = 2625;
|
|
|
|
private const COMMAND_REFS = [
|
|
'fast-feedback' => 'test',
|
|
'confidence' => 'test:confidence',
|
|
'browser' => 'test:browser',
|
|
'heavy-governance' => 'test:heavy',
|
|
'profiling' => 'test:profile',
|
|
'junit' => 'test:junit',
|
|
];
|
|
|
|
private const COMPARISON_BASELINES = [
|
|
'shared-test-fixture-slimming' => [
|
|
'fast-feedback' => [
|
|
'laneId' => 'fast-feedback',
|
|
'finishedAt' => '2026-04-16T13:11:57+00:00',
|
|
'wallClockSeconds' => 176.73623,
|
|
'budgetThresholdSeconds' => 200,
|
|
'targetImprovementPercent' => 10,
|
|
'maxRegressionPercent' => 5,
|
|
],
|
|
'confidence' => [
|
|
'laneId' => 'confidence',
|
|
'finishedAt' => '2026-04-16T13:11:57+00:00',
|
|
'wallClockSeconds' => 394.383441,
|
|
'budgetThresholdSeconds' => 450,
|
|
'targetImprovementPercent' => 10,
|
|
'maxRegressionPercent' => 5,
|
|
],
|
|
],
|
|
];
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function manifest(): array
|
|
{
|
|
return [
|
|
'version' => 3,
|
|
'artifactDirectory' => self::artifactDirectory(),
|
|
'mainlineBranch' => self::mainlineBranch(),
|
|
'classifications' => self::classifications(),
|
|
'families' => self::families(),
|
|
'mixedFileResolutions' => self::mixedFileResolutions(),
|
|
'placementRules' => self::placementRules(),
|
|
'driftGuards' => self::driftGuards(),
|
|
'budgetTargets' => self::budgetTargets(),
|
|
'lanes' => self::lanes(),
|
|
'workflowProfiles' => self::workflowProfiles(),
|
|
'laneBindings' => self::laneBindings(),
|
|
'budgetEnforcementProfiles' => TestLaneBudget::enforcementProfiles(),
|
|
'artifactPublicationContracts' => self::artifactPublicationContracts(),
|
|
'trendContractVersion' => self::laneTrendContractVersion(),
|
|
'laneTrendPolicies' => self::laneTrendPolicies(),
|
|
'failureClasses' => self::failureClasses(),
|
|
'familyBudgets' => self::familyBudgets(),
|
|
'heavyGovernanceBudgetContract' => self::heavyGovernanceBudgetContract(),
|
|
'heavyGovernanceHotspotInventory' => self::heavyGovernanceHotspotInventory(),
|
|
'heavyGovernanceDecompositionRecords' => self::heavyGovernanceDecompositionRecords(),
|
|
'heavyGovernanceSlimmingDecisions' => self::heavyGovernanceSlimmingDecisions(),
|
|
'heavyGovernanceBudgetSnapshots' => self::heavyGovernanceBudgetSnapshots(),
|
|
'heavyGovernanceBudgetOutcome' => self::heavyGovernanceBudgetOutcome(),
|
|
'heavyGovernanceAuthorGuidance' => self::heavyGovernanceAuthorGuidance(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function classifications(): array
|
|
{
|
|
return [
|
|
[
|
|
'classificationId' => 'ui-light',
|
|
'purpose' => 'Localized tenant or admin UI checks that keep narrow operator trust without broad discovery fan-out.',
|
|
'dominantCostDrivers' => ['single-mount', 'localized assertions'],
|
|
'defaultLaneId' => 'confidence',
|
|
'allowedLaneIds' => ['fast-feedback', 'confidence'],
|
|
'forbiddenLaneIds' => ['browser'],
|
|
'reviewerSignals' => [
|
|
'single page or table surface',
|
|
'no broad discovery or reflection pass',
|
|
'bounded assertion count',
|
|
],
|
|
'escalationTriggers' => [
|
|
'multi-surface workflow fan-out',
|
|
'relation-manager breadth',
|
|
'resource or action-surface discovery',
|
|
],
|
|
],
|
|
[
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Bounded operator workflows that still provide meaningful product trust but fan out across more than one surface or step.',
|
|
'dominantCostDrivers' => ['multi-mount workflow', 'fixture fan-out'],
|
|
'defaultLaneId' => 'confidence',
|
|
'allowedLaneIds' => ['confidence', 'heavy-governance'],
|
|
'forbiddenLaneIds' => ['fast-feedback', 'browser'],
|
|
'reviewerSignals' => [
|
|
'multi-step wizard or matrix flow',
|
|
'shared fixture graphs that stay inside one product workflow',
|
|
'direct operator trust value even when setup is dense',
|
|
],
|
|
'escalationTriggers' => [
|
|
'governance-wide discovery',
|
|
'repeated broad relation-manager scans',
|
|
'surface discipline across multiple unrelated resources',
|
|
],
|
|
],
|
|
[
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Broad governance checks that validate action surfaces, relation-manager breadth, or navigation discipline across multiple components.',
|
|
'dominantCostDrivers' => ['surface-wide validation', 'relation-manager breadth', 'assertion density'],
|
|
'defaultLaneId' => 'heavy-governance',
|
|
'allowedLaneIds' => ['heavy-governance'],
|
|
'forbiddenLaneIds' => ['fast-feedback', 'confidence', 'browser'],
|
|
'reviewerSignals' => [
|
|
'governance contract or discipline test',
|
|
'multiple actions or surfaces validated together',
|
|
'broad relation-manager or navigation coverage',
|
|
],
|
|
'escalationTriggers' => [
|
|
'action inventory discovery',
|
|
'header or navigation discipline spanning many resources',
|
|
'matrix or relation-manager breadth beyond one local workflow',
|
|
],
|
|
],
|
|
[
|
|
'classificationId' => 'discovery-heavy',
|
|
'purpose' => 'Discovery, reflection, or remembered-context scans that widen cost as the Filament surface grows.',
|
|
'dominantCostDrivers' => ['resource discovery', 'reflection', 'broad search or registry scans'],
|
|
'defaultLaneId' => 'heavy-governance',
|
|
'allowedLaneIds' => ['heavy-governance'],
|
|
'forbiddenLaneIds' => ['fast-feedback', 'confidence', 'browser'],
|
|
'reviewerSignals' => [
|
|
'reflection-backed discovery',
|
|
'resource or global-search parity scans',
|
|
'remembered environment or registry breadth dominating runtime',
|
|
],
|
|
'escalationTriggers' => [
|
|
'new resources or pages increase the touched surface',
|
|
'broad remembered-context validation',
|
|
'registry or relationship discovery beyond one feature',
|
|
],
|
|
],
|
|
[
|
|
'classificationId' => 'browser',
|
|
'purpose' => 'Real browser smoke and workflow coverage that remains fully isolated from non-browser lanes.',
|
|
'dominantCostDrivers' => ['real-browser interaction', 'end-to-end rendering'],
|
|
'defaultLaneId' => 'browser',
|
|
'allowedLaneIds' => ['browser'],
|
|
'forbiddenLaneIds' => ['fast-feedback', 'confidence', 'heavy-governance', 'profiling', 'junit'],
|
|
'reviewerSignals' => [
|
|
'visit() or browser assertions',
|
|
'real browser session state',
|
|
'JavaScript or DOM continuity checks',
|
|
],
|
|
'escalationTriggers' => [
|
|
'browser runtime or DOM interactivity is required',
|
|
'cross-page journey depends on real browser semantics',
|
|
'client-side behavior is part of the contract',
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function classification(string $classificationId): array
|
|
{
|
|
foreach (self::classifications() as $classification) {
|
|
if ($classification['classificationId'] === $classificationId) {
|
|
return $classification;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown heavy test classification [%s].', $classificationId));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function families(): array
|
|
{
|
|
return [
|
|
[
|
|
'familyId' => 'backup-set-admin-tenant-parity',
|
|
'classificationId' => 'ui-light',
|
|
'purpose' => 'Keep remembered admin tenant scoping honest on a localized backup-set listing surface.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'confidence',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-light',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Document the retained localized UI confidence anchor.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/BackupSetAdminTenantParityTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical ui-light confidence example for Spec 208.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/BackupSetAdminTenantParityTest.php',
|
|
],
|
|
'costSignals' => ['single-mount', 'remembered environment state', 'localized table assertions'],
|
|
'confidenceRationale' => 'This remains in Confidence because it protects a narrow remembered-tenant table surface without broad discovery cost.',
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'baseline-compare-matrix-workflow',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Validate the bounded baseline compare matrix workflow, including visible-assignment fan-out and dense matrix rendering.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'confidence',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the matrix workflow explicit as retained confidence coverage.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded matrix workflow action hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded matrix workflow builder hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
|
|
'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
|
|
'tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php',
|
|
],
|
|
'costSignals' => ['workflow fan-out', 'dense fixture graph', 'multi-tenant matrix rendering'],
|
|
'confidenceRationale' => 'The compare matrix remains a primary operator workflow and stays bounded to one feature surface even when fixtures are dense.',
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'onboarding-wizard-enforcement',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Preserve the managed environment onboarding wizard capability flow as an intentional retained workflow check.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'confidence',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Retain bounded wizard trust in Confidence with explicit classification.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded onboarding workflow hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
|
|
],
|
|
'costSignals' => ['multi-step wizard flow', 'repeated Livewire mounts', 'workspace capability transitions'],
|
|
'confidenceRationale' => 'The onboarding wizard is a high-value operator path that should remain in Confidence despite its bounded workflow cost.',
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'finding-bulk-actions-workflow',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Escalate the large finding bulk-action workflow once multi-action fan-out and audit density stop fitting the standard contributor loops.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the bulk finding workflow explicitly cataloged as workflow-heavy even after it leaves the standard lanes.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Findings/FindingBulkActionsTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical high-fan-out finding bulk workflow hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Findings/FindingBulkActionsTest.php',
|
|
],
|
|
'costSignals' => ['bulk table actions', 'multi-action workflow fan-out', 'high-volume audit assertions'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'drift-bulk-triage-all-matching',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Escalate the all-matching drift triage confirmation flow once it operates over a large result set and broad action fan-out.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the all-matching drift triage confirmation flow classified as workflow-heavy while routing it intentionally to Heavy Governance.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical all-matching drift triage hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
|
|
],
|
|
'costSignals' => ['all-matching bulk action', 'typed confirmation flow', 'large result-set workflow'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'baseline-profile-start-surfaces',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Escalate the baseline profile capture and compare start surfaces once repeated header-action gating and queued-operation setup dominate runtime.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the baseline profile start surfaces classified as workflow-heavy while routing them intentionally to Heavy Governance.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical baseline compare start-surface hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical baseline capture start-surface hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical baseline action authorization hotspot tied to the same start-surface family.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
|
|
],
|
|
'costSignals' => ['repeated Livewire page mounts', 'header action gating matrix', 'queued-operation launch workflow'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'workspace-settings-slice-management',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Escalate the multi-slice workspace settings management surface once one test covers a broad save, reset, and resolver verification workflow across several settings domains.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the workspace settings management surface classified as a broad workflow-heavy check while routing it out of Confidence.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical workspace settings multi-slice hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
|
|
],
|
|
'costSignals' => ['multi-slice form workflow', 'save-and-reset path coverage', 'post-write resolver verification fan-out'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'findings-workflow-surfaces',
|
|
'classificationId' => 'ui-workflow',
|
|
'purpose' => 'Escalate the remaining row, view, filter, and renewal findings workflow surfaces once their multi-step state transitions and evidence handling dominate confidence runtime.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'ui-workflow',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep the findings workflow surface explicitly cataloged as workflow-heavy while routing it out of Confidence.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical row-action workflow hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical view-header workflow hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Findings/FindingsListFiltersTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical findings filter workflow hotspot.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Findings/FindingExceptionRenewalTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical exception renewal workflow hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
|
|
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
|
|
'tests/Feature/Findings/FindingsListFiltersTest.php',
|
|
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
|
|
],
|
|
'costSignals' => ['multi-step workflow transitions', 'filter-state persistence', 'evidence and audit verification'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'workspace-only-admin-surface-independence',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Escalate cross-resource workspace-only admin surface independence checks because they validate broad remembered-tenant behavior across several Filament resources together.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Treat workspace-only cross-resource independence as a broad surface guard.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical workspace-only admin surface independence hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
|
|
],
|
|
'costSignals' => ['cross-resource surface assertions', 'remembered environment context variance', 'workspace-only admin invariants'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'action-surface-contract',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Guard broad action-surface discovery and declaration discipline across pages, resources, and relation managers.',
|
|
'currentLaneId' => 'heavy-governance',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Flag broad governance validation separately from retained workflows.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Guards/ActionSurfaceContractTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical seeded surface-guard hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Guards/ActionSurfaceContractTest.php',
|
|
],
|
|
'costSignals' => ['resource discovery', 'surface-wide validation', 'broad assertion density'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'provider-dispatch-gate-coverage',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Keep the first-slice provider-backed start hosts on canonical ProviderOperationStartGate-owned entry points instead of route-bounded direct-dispatch bypasses.',
|
|
'currentLaneId' => 'heavy-governance',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'The bypass guard spans multiple route-bounded start surfaces and belongs with heavy governance checks.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Guards/ProviderDispatchGateCoverageTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Canonical guard for first-slice provider-backed dispatch-gate coverage.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Guards/ProviderDispatchGateCoverageTest.php',
|
|
],
|
|
'costSignals' => ['route-bounded surface scan', 'start-host governance breadth', 'gate adoption regression detection'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'ui-bloat-regression-guard',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Guard known UI bloat and customer/auditor safety regression patterns across configured runtime UI source paths.',
|
|
'currentLaneId' => 'heavy-governance',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'UI bloat scanning spans multiple source surfaces and belongs to heavy governance.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Guards/UiBloatRegressionGuardTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Spec 375 source-scan guard entrypoint.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Guards/UiBloatRegressionGuardTest.php',
|
|
],
|
|
'costSignals' => ['source-scan guard inventory', 'customer-safety leakage heuristics', 'manual-review finding classification'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'policy-resource-admin-search-parity',
|
|
'classificationId' => 'discovery-heavy',
|
|
'purpose' => 'Verify remembered canonical admin tenant state never leaks policy global-search discovery.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'discovery-heavy',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Explicitly classify broad admin search parity as discovery-heavy.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded policy resource discovery hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
|
|
],
|
|
'costSignals' => ['reflection', 'global-search discovery', 'remembered admin tenant context'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'policy-version-admin-search-parity',
|
|
'classificationId' => 'discovery-heavy',
|
|
'purpose' => 'Keep policy-version admin search parity classified as discovery-heavy when remembered context broadens the surface.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'discovery-heavy',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Treat policy-version parity as the same discovery-heavy family class as resource parity.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded policy-version discovery hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
|
|
],
|
|
'costSignals' => ['reflection', 'global-search discovery', 'remembered admin tenant context'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'backup-items-relation-manager-enforcement',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Protect broad relation-manager authorization affordances for backup item actions from drifting into lighter lanes.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Relation-manager breadth belongs to the surface-guard class.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded backup items relation-manager hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
|
|
],
|
|
'costSignals' => ['relation-manager breadth', 'table action matrix', 'tenant capability transitions'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'workspace-memberships-relation-manager-enforcement',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Keep workspace membership relation-manager action discipline classified as broad surface governance.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Workspace membership relation-manager breadth is a deliberate surface guard.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded workspace memberships relation-manager hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
|
|
],
|
|
'costSignals' => ['relation-manager breadth', 'action visibility matrix', 'workspace membership transitions'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'environment-review-header-discipline',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Enforce primary and grouped header action discipline for environment review pages.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Header action discipline spans a broad action surface.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/EnvironmentReviewHeaderDisciplineTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded header discipline hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/EnvironmentReviewHeaderDisciplineTest.php',
|
|
],
|
|
'costSignals' => ['header action discipline', 'grouped action inventory', 'multi-state surface assertions'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'panel-navigation-segregation',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Keep panel navigation segregation and tenant-sensitive registration checks in the heavy governance lane.',
|
|
'currentLaneId' => 'confidence',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Navigation discipline is a broad surface-guard concern.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Filament/PanelNavigationSegregationTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded navigation discipline hotspot.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/PanelNavigationSegregationTest.php',
|
|
],
|
|
'costSignals' => ['navigation registration breadth', 'panel boot', 'tenant-sensitive surface assertions'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'ops-ux-governance',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Keep established Ops UX, alert header, and credential leak governance checks under the heavy-governance umbrella.',
|
|
'currentLaneId' => 'heavy-governance',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'surface-guard',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Treat the established Ops UX batch as broad governance coverage.',
|
|
],
|
|
[
|
|
'selectorType' => 'path',
|
|
'selectorValue' => 'tests/Feature/OpsUx',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Existing broad Ops UX directory remains heavy-governance owned.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php',
|
|
'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php',
|
|
'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php',
|
|
],
|
|
'costSignals' => ['ops-ux governance breadth', 'cross-surface workflow coverage', 'alert and credential guard fan-out'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'no-legacy-guardrail',
|
|
'classificationId' => 'surface-guard',
|
|
'purpose' => 'Keep the Spec 288 no-legacy route/helper and provider/role guardrails isolated inside heavy-governance ownership.',
|
|
'currentLaneId' => 'heavy-governance',
|
|
'targetLaneId' => 'heavy-governance',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Guards/Spec288NoLegacyRouteAndHelperGuardTest.php',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Spec 288 route and helper source-scan guard remains heavy-governance-only.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Feature/Guards/Spec288ProviderCoreAndRoleAuthorityGuardTest.php',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Spec 288 provider-core and role-authority seam guard remains heavy-governance-only.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Feature/Guards/Spec288NoLegacyRouteAndHelperGuardTest.php',
|
|
'tests/Feature/Guards/Spec288ProviderCoreAndRoleAuthorityGuardTest.php',
|
|
],
|
|
'costSignals' => ['source-scan guard inventory', 'provider-boundary contract enforcement', 'cutover drift coverage'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
[
|
|
'familyId' => 'browser-smoke',
|
|
'classificationId' => 'browser',
|
|
'purpose' => 'Isolate browser smoke and real DOM continuity checks in the dedicated browser lane.',
|
|
'currentLaneId' => 'browser',
|
|
'targetLaneId' => 'browser',
|
|
'selectors' => [
|
|
[
|
|
'selectorType' => 'group',
|
|
'selectorValue' => 'browser',
|
|
'selectorRole' => 'include',
|
|
'sourceOfTruth' => 'pest-group',
|
|
'rationale' => 'Keep browser ownership explicit at the group layer.',
|
|
],
|
|
[
|
|
'selectorType' => 'path',
|
|
'selectorValue' => 'tests/Browser',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Attribute the browser lane to its dedicated suite path.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Seeded browser hotspot for Spec 208.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Spec 288 keeps the provider-connection browser continuity anchor explicit.',
|
|
],
|
|
[
|
|
'selectorType' => 'file',
|
|
'selectorValue' => 'tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php',
|
|
'selectorRole' => 'inventory-only',
|
|
'sourceOfTruth' => 'manifest',
|
|
'rationale' => 'Spec 288 keeps the workspace RBAC browser continuity anchor explicit.',
|
|
],
|
|
],
|
|
'hotspotFiles' => [
|
|
'tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php',
|
|
'tests/Browser/Spec281ProviderConnectionScopeSmokeTest.php',
|
|
'tests/Browser/Spec285WorkspaceRbacEnvironmentAccessSmokeTest.php',
|
|
],
|
|
'costSignals' => ['real-browser interaction', 'dense DOM continuity', 'end-to-end smoke'],
|
|
'validationStatus' => 'guarded',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function family(string $familyId): array
|
|
{
|
|
foreach (self::families() as $family) {
|
|
if ($family['familyId'] === $familyId) {
|
|
return $family;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown heavy test family [%s].', $familyId));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function familiesByTargetLane(string $laneId): array
|
|
{
|
|
return array_values(array_filter(
|
|
self::families(),
|
|
static fn (array $family): bool => $family['targetLaneId'] === $laneId,
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function mixedFileResolutions(): array
|
|
{
|
|
return [
|
|
[
|
|
'filePath' => 'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
|
|
'primaryClassificationId' => 'discovery-heavy',
|
|
'secondaryClassificationIds' => ['ui-light'],
|
|
'resolutionStrategy' => 'broadest-cost-wins',
|
|
'rationale' => 'The file reads like a narrow admin check but remembered environment search parity and reflection-backed discovery dominate the runtime shape.',
|
|
'followUpRequired' => true,
|
|
],
|
|
[
|
|
'filePath' => 'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
|
|
'primaryClassificationId' => 'ui-workflow',
|
|
'secondaryClassificationIds' => ['discovery-heavy'],
|
|
'resolutionStrategy' => 'broadest-cost-wins',
|
|
'rationale' => 'The builder is dense and discovery aware, but it still primarily protects the bounded compare-matrix workflow that Confidence keeps.',
|
|
'followUpRequired' => true,
|
|
],
|
|
[
|
|
'filePath' => 'tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php',
|
|
'primaryClassificationId' => 'ui-workflow',
|
|
'secondaryClassificationIds' => ['discovery-heavy', 'surface-guard'],
|
|
'resolutionStrategy' => 'broadest-cost-wins',
|
|
'rationale' => 'The fixture trait amplifies the compare-matrix workflow family until a narrower extraction is justified.',
|
|
'followUpRequired' => true,
|
|
],
|
|
[
|
|
'filePath' => 'tests/Feature/Concerns/BuildsPortfolioTriageFixtures.php',
|
|
'primaryClassificationId' => 'surface-guard',
|
|
'secondaryClassificationIds' => ['ui-workflow'],
|
|
'resolutionStrategy' => 'broadest-cost-wins',
|
|
'rationale' => 'The portfolio triage fixture graph widens cross-tenant scope and should inherit the broadest governance-oriented cost signal until split.',
|
|
'followUpRequired' => true,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public static function mixedFileResolution(string $filePath): ?array
|
|
{
|
|
foreach (self::mixedFileResolutions() as $resolution) {
|
|
if ($resolution['filePath'] === $filePath) {
|
|
return $resolution;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function placementRules(): array
|
|
{
|
|
return [
|
|
[
|
|
'ruleId' => 'ui-light-fast-feedback-allowed',
|
|
'classificationId' => 'ui-light',
|
|
'laneId' => 'fast-feedback',
|
|
'allowance' => 'allowed',
|
|
'reason' => 'Explicitly approved localized UI checks may remain in Fast Feedback when discovery stays bounded.',
|
|
],
|
|
[
|
|
'ruleId' => 'ui-light-confidence-required',
|
|
'classificationId' => 'ui-light',
|
|
'laneId' => 'confidence',
|
|
'allowance' => 'required',
|
|
'reason' => 'Confidence keeps localized UI trust as its lightest retained UI layer.',
|
|
],
|
|
[
|
|
'ruleId' => 'ui-workflow-confidence-required',
|
|
'classificationId' => 'ui-workflow',
|
|
'laneId' => 'confidence',
|
|
'allowance' => 'required',
|
|
'reason' => 'Selected bounded workflows remain in Confidence to preserve real operator trust.',
|
|
],
|
|
[
|
|
'ruleId' => 'ui-workflow-heavy-governance-allowed',
|
|
'classificationId' => 'ui-workflow',
|
|
'laneId' => 'heavy-governance',
|
|
'allowance' => 'allowed',
|
|
'reason' => 'Workflow families may escalate into Heavy Governance when their breadth stops being local.',
|
|
],
|
|
[
|
|
'ruleId' => 'surface-guard-heavy-governance-required',
|
|
'classificationId' => 'surface-guard',
|
|
'laneId' => 'heavy-governance',
|
|
'allowance' => 'required',
|
|
'reason' => 'Broad surface guards belong in Heavy Governance by default.',
|
|
],
|
|
[
|
|
'ruleId' => 'surface-guard-fast-feedback-forbidden',
|
|
'classificationId' => 'surface-guard',
|
|
'laneId' => 'fast-feedback',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Fast Feedback must never carry broad surface-guard breadth.',
|
|
],
|
|
[
|
|
'ruleId' => 'surface-guard-confidence-forbidden',
|
|
'classificationId' => 'surface-guard',
|
|
'laneId' => 'confidence',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Confidence should retain bounded workflows, not governance-wide surface discipline.',
|
|
],
|
|
[
|
|
'ruleId' => 'discovery-heavy-heavy-governance-required',
|
|
'classificationId' => 'discovery-heavy',
|
|
'laneId' => 'heavy-governance',
|
|
'allowance' => 'required',
|
|
'reason' => 'Discovery-heavy scans scale with surface breadth and therefore belong in Heavy Governance.',
|
|
],
|
|
[
|
|
'ruleId' => 'discovery-heavy-fast-feedback-forbidden',
|
|
'classificationId' => 'discovery-heavy',
|
|
'laneId' => 'fast-feedback',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Fast Feedback must stay free of discovery-heavy scans.',
|
|
],
|
|
[
|
|
'ruleId' => 'discovery-heavy-confidence-forbidden',
|
|
'classificationId' => 'discovery-heavy',
|
|
'laneId' => 'confidence',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Confidence keeps explicit retained UI trust, not remembered-context discovery scans.',
|
|
],
|
|
[
|
|
'ruleId' => 'browser-browser-required',
|
|
'classificationId' => 'browser',
|
|
'laneId' => 'browser',
|
|
'allowance' => 'required',
|
|
'reason' => 'Browser coverage remains isolated in the dedicated browser lane.',
|
|
],
|
|
[
|
|
'ruleId' => 'browser-fast-feedback-forbidden',
|
|
'classificationId' => 'browser',
|
|
'laneId' => 'fast-feedback',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Browser coverage cannot run in Fast Feedback.',
|
|
],
|
|
[
|
|
'ruleId' => 'browser-confidence-forbidden',
|
|
'classificationId' => 'browser',
|
|
'laneId' => 'confidence',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Browser coverage cannot run in Confidence.',
|
|
],
|
|
[
|
|
'ruleId' => 'browser-heavy-governance-forbidden',
|
|
'classificationId' => 'browser',
|
|
'laneId' => 'heavy-governance',
|
|
'allowance' => 'forbidden',
|
|
'reason' => 'Browser coverage does not merge into Heavy Governance.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function driftGuards(): array
|
|
{
|
|
return [
|
|
[
|
|
'guardId' => 'browser-lane-isolation',
|
|
'scope' => 'classification',
|
|
'assertionType' => 'browser-isolation',
|
|
'targetRefs' => ['browser'],
|
|
'owningTestPaths' => ['tests/Feature/Guards/BrowserLaneIsolationTest.php'],
|
|
'failureContract' => 'Failures must name the browser file or family that leaked into a non-browser lane.',
|
|
],
|
|
[
|
|
'guardId' => 'discovery-heavy-fast-feedback-exclusion',
|
|
'scope' => 'lane',
|
|
'assertionType' => 'forbidden-membership',
|
|
'targetRefs' => ['discovery-heavy', 'fast-feedback'],
|
|
'owningTestPaths' => [
|
|
'tests/Feature/Guards/FastFeedbackLaneContractTest.php',
|
|
'tests/Feature/Guards/FastFeedbackLaneExclusionTest.php',
|
|
],
|
|
'failureContract' => 'Failures must name the discovery-heavy family or file still present in Fast Feedback.',
|
|
],
|
|
[
|
|
'guardId' => 'surface-guard-fast-feedback-exclusion',
|
|
'scope' => 'lane',
|
|
'assertionType' => 'forbidden-membership',
|
|
'targetRefs' => ['surface-guard', 'fast-feedback'],
|
|
'owningTestPaths' => [
|
|
'tests/Feature/Guards/FastFeedbackLaneContractTest.php',
|
|
'tests/Feature/Guards/FastFeedbackLaneExclusionTest.php',
|
|
],
|
|
'failureContract' => 'Failures must identify the broad surface-guard file or family that still leaks into Fast Feedback.',
|
|
],
|
|
[
|
|
'guardId' => 'confidence-ui-workflow-whitelist',
|
|
'scope' => 'lane',
|
|
'assertionType' => 'required-membership',
|
|
'targetRefs' => ['ui-light', 'ui-workflow', 'confidence'],
|
|
'owningTestPaths' => [
|
|
'tests/Feature/Guards/ConfidenceLaneContractTest.php',
|
|
'tests/Feature/Guards/HeavyGovernanceLaneContractTest.php',
|
|
],
|
|
'failureContract' => 'Failures must name the confidence family whose classification or rationale is inconsistent with the retained confidence catalog.',
|
|
],
|
|
[
|
|
'guardId' => 'heavy-attribution-contract',
|
|
'scope' => 'report',
|
|
'assertionType' => 'required-attribution',
|
|
'targetRefs' => ['surface-guard', 'discovery-heavy', 'browser'],
|
|
'owningTestPaths' => [
|
|
'tests/Feature/Guards/TestLaneArtifactsContractTest.php',
|
|
'tests/Feature/Guards/ProfileLaneContractTest.php',
|
|
],
|
|
'failureContract' => 'Failures must name the missing classification or family attribution payload under storage/logs/test-lanes.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function budgetTargets(): array
|
|
{
|
|
return [
|
|
[
|
|
'budgetId' => 'lane-heavy-governance',
|
|
'targetType' => 'lane',
|
|
'targetId' => 'heavy-governance',
|
|
'thresholdSeconds' => self::recommendedHeavyGovernanceNormalizedThreshold(),
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => self::heavyGovernanceBudgetContract()['lifecycleState'],
|
|
'reviewCadence' => 'refresh when heavy family ownership changes',
|
|
],
|
|
[
|
|
'budgetId' => 'classification-ui-workflow',
|
|
'targetType' => 'classification',
|
|
'targetId' => 'ui-workflow',
|
|
'thresholdSeconds' => 150,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after the retained confidence families stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'classification-surface-guard',
|
|
'targetType' => 'classification',
|
|
'targetId' => 'surface-guard',
|
|
'thresholdSeconds' => 90,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'classification-discovery-heavy',
|
|
'targetType' => 'classification',
|
|
'targetId' => 'discovery-heavy',
|
|
'thresholdSeconds' => 60,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'classification-browser',
|
|
'targetType' => 'classification',
|
|
'targetId' => 'browser',
|
|
'thresholdSeconds' => 150,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable browser runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-backup-set-admin-tenant-parity',
|
|
'targetType' => 'family',
|
|
'targetId' => 'backup-set-admin-tenant-parity',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten if more localized admin parity families are cataloged',
|
|
],
|
|
[
|
|
'budgetId' => 'family-baseline-compare-matrix-workflow',
|
|
'targetType' => 'family',
|
|
'targetId' => 'baseline-compare-matrix-workflow',
|
|
'thresholdSeconds' => 75,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after confidence workflow coverage settles',
|
|
],
|
|
[
|
|
'budgetId' => 'family-onboarding-wizard-enforcement',
|
|
'targetType' => 'family',
|
|
'targetId' => 'onboarding-wizard-enforcement',
|
|
'thresholdSeconds' => 45,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after confidence workflow coverage settles',
|
|
],
|
|
[
|
|
'budgetId' => 'family-finding-bulk-actions-workflow',
|
|
'targetType' => 'family',
|
|
'targetId' => 'finding-bulk-actions-workflow',
|
|
'thresholdSeconds' => 90,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-drift-bulk-triage-all-matching',
|
|
'targetType' => 'family',
|
|
'targetId' => 'drift-bulk-triage-all-matching',
|
|
'thresholdSeconds' => 25,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-baseline-profile-start-surfaces',
|
|
'targetType' => 'family',
|
|
'targetId' => 'baseline-profile-start-surfaces',
|
|
'thresholdSeconds' => 140,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-workspace-settings-slice-management',
|
|
'targetType' => 'family',
|
|
'targetId' => 'workspace-settings-slice-management',
|
|
'thresholdSeconds' => 70,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-findings-workflow-surfaces',
|
|
'targetType' => 'family',
|
|
'targetId' => 'findings-workflow-surfaces',
|
|
'thresholdSeconds' => 130,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-workspace-only-admin-surface-independence',
|
|
'targetType' => 'family',
|
|
'targetId' => 'workspace-only-admin-surface-independence',
|
|
'thresholdSeconds' => 40,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'report-only',
|
|
'lifecycleState' => 'draft',
|
|
'reviewCadence' => 'tighten after escalated heavy workflows stabilize',
|
|
],
|
|
[
|
|
'budgetId' => 'family-action-surface-contract',
|
|
'targetType' => 'family',
|
|
'targetId' => 'action-surface-contract',
|
|
'thresholdSeconds' => 35,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-provider-dispatch-gate-coverage',
|
|
'targetType' => 'family',
|
|
'targetId' => 'provider-dispatch-gate-coverage',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-policy-resource-admin-search-parity',
|
|
'targetType' => 'family',
|
|
'targetId' => 'policy-resource-admin-search-parity',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-policy-version-admin-search-parity',
|
|
'targetType' => 'family',
|
|
'targetId' => 'policy-version-admin-search-parity',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-backup-items-relation-manager-enforcement',
|
|
'targetType' => 'family',
|
|
'targetId' => 'backup-items-relation-manager-enforcement',
|
|
'thresholdSeconds' => 30,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-workspace-memberships-relation-manager-enforcement',
|
|
'targetType' => 'family',
|
|
'targetId' => 'workspace-memberships-relation-manager-enforcement',
|
|
'thresholdSeconds' => 25,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-environment-review-header-discipline',
|
|
'targetType' => 'family',
|
|
'targetId' => 'environment-review-header-discipline',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-panel-navigation-segregation',
|
|
'targetType' => 'family',
|
|
'targetId' => 'panel-navigation-segregation',
|
|
'thresholdSeconds' => 20,
|
|
'baselineSource' => 'measured-post-spec-207',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable heavy-governance runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-ops-ux-governance',
|
|
'targetType' => 'family',
|
|
'targetId' => 'ops-ux-governance',
|
|
'thresholdSeconds' => 120,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
[
|
|
'budgetId' => 'family-browser-smoke',
|
|
'targetType' => 'family',
|
|
'targetId' => 'browser-smoke',
|
|
'thresholdSeconds' => 150,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function familyBudgets(): array
|
|
{
|
|
$familyBudgets = [];
|
|
|
|
foreach (self::budgetTargets() as $budgetTarget) {
|
|
if (($budgetTarget['targetType'] ?? null) !== 'family') {
|
|
continue;
|
|
}
|
|
|
|
$family = self::family((string) $budgetTarget['targetId']);
|
|
$selectors = array_values(array_unique(array_merge(
|
|
self::budgetSelectorsForFamily($family),
|
|
self::familyHotspotTestFiles($family),
|
|
)));
|
|
|
|
$familyBudgets[] = array_merge($budgetTarget, [
|
|
'familyId' => $budgetTarget['targetId'],
|
|
'selectorType' => self::primaryBudgetSelectorType($family),
|
|
'selectors' => $selectors,
|
|
]);
|
|
}
|
|
|
|
return $familyBudgets;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function heavyGovernanceBudgetContract(?float $measuredSeconds = null): array
|
|
{
|
|
$measuredSeconds ??= self::heavyGovernanceCurrentMeasuredSeconds();
|
|
$normalizedThresholdSeconds = self::recommendedHeavyGovernanceNormalizedThreshold($measuredSeconds);
|
|
$decisionStatus = $normalizedThresholdSeconds > self::heavyGovernanceSummaryThresholdSeconds()
|
|
? 'recalibrated'
|
|
: 'recovered';
|
|
|
|
return [
|
|
'laneId' => 'heavy-governance',
|
|
'summaryThresholdSeconds' => self::heavyGovernanceSummaryThresholdSeconds(),
|
|
'evaluationThresholdSeconds' => self::heavyGovernanceLegacyEvaluationThresholdSeconds(),
|
|
'normalizedThresholdSeconds' => $normalizedThresholdSeconds,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcementLevel' => 'warn',
|
|
'lifecycleState' => $decisionStatus === 'recalibrated' ? 'recalibrated' : 'documented',
|
|
'reconciliationRationale' => $decisionStatus === 'recalibrated'
|
|
? sprintf(
|
|
'Spec 209 removed duplicated workflow fan-out in the baseline-profile and findings-heavy families, but the settled lane still retains intentional surface-guard depth and the workspace settings residual workflow cost; the normalized contract is %.0fs after the honest rerun.',
|
|
$normalizedThresholdSeconds,
|
|
)
|
|
: 'Spec 209 removed enough duplicate workflow work for the heavy-governance lane to recover within the pre-normalization 300s contract.',
|
|
'decisionStatus' => $decisionStatus,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function heavyGovernanceHotspotInventory(): array
|
|
{
|
|
$measuredSecondsByFamily = self::heavyGovernanceBaselineMeasuredSecondsByFamily();
|
|
|
|
$inventoryMetadata = [
|
|
'baseline-profile-start-surfaces' => ['costDriverCategory' => 'workflow-heavy', 'priorityTier' => 'primary', 'status' => 'slimmed'],
|
|
'action-surface-contract' => ['costDriverCategory' => 'intentionally-heavy', 'priorityTier' => 'primary', 'status' => 'retained'],
|
|
'ops-ux-governance' => ['costDriverCategory' => 'intentionally-heavy', 'priorityTier' => 'primary', 'status' => 'retained'],
|
|
'findings-workflow-surfaces' => ['costDriverCategory' => 'workflow-heavy', 'priorityTier' => 'primary', 'status' => 'slimmed'],
|
|
'finding-bulk-actions-workflow' => ['costDriverCategory' => 'redundant', 'priorityTier' => 'primary', 'status' => 'slimmed'],
|
|
'workspace-settings-slice-management' => ['costDriverCategory' => 'helper-driven', 'priorityTier' => 'primary', 'status' => 'follow-up'],
|
|
];
|
|
|
|
$inventory = [];
|
|
|
|
foreach ($inventoryMetadata as $familyId => $metadata) {
|
|
$family = self::family($familyId);
|
|
$budgetTarget = self::budgetTarget('family', $familyId);
|
|
|
|
$inventory[] = [
|
|
'familyId' => $familyId,
|
|
'classificationId' => $family['classificationId'],
|
|
'purpose' => $family['purpose'],
|
|
'measuredSeconds' => round((float) ($measuredSecondsByFamily[$familyId] ?? 0.0), 6),
|
|
'hotspotFiles' => $family['hotspotFiles'],
|
|
'costDriverCategory' => $metadata['costDriverCategory'],
|
|
'priorityTier' => $metadata['priorityTier'],
|
|
'currentBudgetSeconds' => isset($budgetTarget['thresholdSeconds']) ? (float) $budgetTarget['thresholdSeconds'] : null,
|
|
'status' => $metadata['status'],
|
|
];
|
|
}
|
|
|
|
usort($inventory, static fn (array $left, array $right): int => $right['measuredSeconds'] <=> $left['measuredSeconds']);
|
|
|
|
return $inventory;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function heavyGovernanceDecompositionRecords(): array
|
|
{
|
|
return [
|
|
[
|
|
'familyId' => 'baseline-profile-start-surfaces',
|
|
'trustType' => 'workflow-trust',
|
|
'requiredBreadth' => 'One capture start surface, one compare start surface, and one authorization matrix on the baseline profile detail page.',
|
|
'duplicateWorkSources' => ['repeated-livewire-mounts', 'header-action-gating-matrix', 'duplicate-baseline-fixture-seeding'],
|
|
'duplicateWorkEstimateSeconds' => 12.0,
|
|
'residualCostSource' => 'family-breadth',
|
|
'recommendedAction' => 'centralize-work',
|
|
'notes' => 'The slimming pass keeps the capture and compare start-surface trust intact while reducing repeated Livewire page mounts and repeated start-surface gating setup.',
|
|
],
|
|
[
|
|
'familyId' => 'findings-workflow-surfaces',
|
|
'trustType' => 'workflow-trust',
|
|
'requiredBreadth' => 'Row actions, view-header actions, list-filter persistence, and renewal workflows remain separate trust slices but now share less repeated mounting work.',
|
|
'duplicateWorkSources' => ['repeated-livewire-mounts', 'filter-state-persistence', 'audit-fan-out'],
|
|
'duplicateWorkEstimateSeconds' => 6.0,
|
|
'residualCostSource' => 'family-breadth',
|
|
'recommendedAction' => 'centralize-work',
|
|
'notes' => 'The row and view workflow tests shed repeated page mounts while keeping state-transition, assignment, and audit guarantees explicit.',
|
|
],
|
|
[
|
|
'familyId' => 'finding-bulk-actions-workflow',
|
|
'trustType' => 'workflow-trust',
|
|
'requiredBreadth' => 'Bulk triage, assign, resolve, and close still need per-record workflow and audit verification, but the selected-record fan-out no longer needs a triple-digit fixture count.',
|
|
'duplicateWorkSources' => ['audit-fan-out', 'duplicate bulk-action setup'],
|
|
'duplicateWorkEstimateSeconds' => 20.0,
|
|
'residualCostSource' => 'family-breadth',
|
|
'recommendedAction' => 'narrow-assertions',
|
|
'notes' => 'The family now validates per-record audit fan-out with a representative selected set instead of an unnecessarily large fixture batch.',
|
|
],
|
|
[
|
|
'familyId' => 'action-surface-contract',
|
|
'trustType' => 'surface-trust',
|
|
'requiredBreadth' => 'The contract still needs broad action-surface discovery across resources, pages, and relation managers.',
|
|
'duplicateWorkSources' => ['resource-discovery-pass', 'surface-wide validation'],
|
|
'residualCostSource' => 'intentional-depth',
|
|
'recommendedAction' => 'retain-as-heavy',
|
|
'notes' => 'No repeatable duplication outweighed the intentionally broad governance surface, so the family remains heavy by design.',
|
|
],
|
|
[
|
|
'familyId' => 'ops-ux-governance',
|
|
'trustType' => 'surface-trust',
|
|
'requiredBreadth' => 'Ops UX still spans multiple monitoring, notification, and run-detail surfaces with legitimate governance depth.',
|
|
'duplicateWorkSources' => ['surface-wide validation', 'cross-surface workflow coverage'],
|
|
'residualCostSource' => 'intentional-depth',
|
|
'recommendedAction' => 'retain-as-heavy',
|
|
'notes' => 'The audit found real breadth rather than a removable duplicate pass, so the family is explicitly retained as intentional heavy coverage.',
|
|
],
|
|
[
|
|
'familyId' => 'workspace-settings-slice-management',
|
|
'trustType' => 'workflow-trust',
|
|
'requiredBreadth' => 'The settings surface still needs multi-slice save and reset verification, but its residual cost is mostly service and resolver fan-out rather than broad UI discovery.',
|
|
'duplicateWorkSources' => ['post-write resolver verification', 'multi-slice form workflow'],
|
|
'residualCostSource' => 'helper-driven',
|
|
'recommendedAction' => 'route-follow-up',
|
|
'notes' => 'Spec 209 records the residual settings cost explicitly instead of disguising it as a family-width win.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function heavyGovernanceSlimmingDecisions(): array
|
|
{
|
|
return [
|
|
[
|
|
'familyId' => 'baseline-profile-start-surfaces',
|
|
'decisionType' => 'centralize',
|
|
'scope' => [
|
|
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
|
|
],
|
|
'guardPreservationPlan' => 'Keep capture and compare launch authorization, rollout gating, and invalid-scope rejection on the real profile detail page while trimming repeated mount overhead.',
|
|
'expectedDeltaSeconds' => 12.0,
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => [
|
|
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
|
|
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
|
|
'scripts/platform-test-lane heavy-governance',
|
|
],
|
|
],
|
|
[
|
|
'familyId' => 'findings-workflow-surfaces',
|
|
'decisionType' => 'centralize',
|
|
'scope' => [
|
|
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
|
|
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
|
|
'tests/Feature/Findings/FindingsListFiltersTest.php',
|
|
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
|
|
],
|
|
'guardPreservationPlan' => 'Preserve workflow-state, filter-persistence, and renewal evidence assertions while reducing repeated component bootstraps.',
|
|
'expectedDeltaSeconds' => 6.0,
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => [
|
|
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
|
|
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
|
|
'tests/Feature/Findings/FindingsListFiltersTest.php',
|
|
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
|
|
],
|
|
],
|
|
[
|
|
'familyId' => 'finding-bulk-actions-workflow',
|
|
'decisionType' => 'trim-duplicate-work',
|
|
'scope' => ['tests/Feature/Findings/FindingBulkActionsTest.php'],
|
|
'guardPreservationPlan' => 'Keep per-record workflow transitions and audit assertions for each bulk action while cutting the oversized selected-record fan-out.',
|
|
'expectedDeltaSeconds' => 20.0,
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => [
|
|
'tests/Feature/Findings/FindingBulkActionsTest.php',
|
|
'scripts/platform-test-lane heavy-governance',
|
|
],
|
|
],
|
|
[
|
|
'familyId' => 'action-surface-contract',
|
|
'decisionType' => 'retain',
|
|
'guardPreservationPlan' => 'Retain the broad action-surface discovery contract until a future change proves a genuinely duplicate discovery pass.',
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => ['tests/Feature/Guards/ActionSurfaceContractTest.php'],
|
|
],
|
|
[
|
|
'familyId' => 'ops-ux-governance',
|
|
'decisionType' => 'retain',
|
|
'guardPreservationPlan' => 'Keep the broad Ops UX governance surface intact because the current cost comes from intentional coverage breadth, not an accidental duplicate loop.',
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => [
|
|
'tests/Feature/OpsUx/OperationCatalogCoverageTest.php',
|
|
'tests/Feature/OpsUx/OperateHubShellTest.php',
|
|
'tests/Feature/OpsUx/ActiveRunsTest.php',
|
|
],
|
|
],
|
|
[
|
|
'familyId' => 'workspace-settings-slice-management',
|
|
'decisionType' => 'follow-up',
|
|
'scope' => [
|
|
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
|
|
'tests/Support/TestLaneManifest.php',
|
|
],
|
|
'guardPreservationPlan' => 'Keep the current multi-slice save and reset assertions intact while recording the residual helper-driven cost for later follow-up.',
|
|
'owner' => 'platform-test-governance',
|
|
'validationPlan' => ['tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php'],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function heavyGovernanceBudgetSnapshots(): array
|
|
{
|
|
return [
|
|
self::seededHeavyGovernanceBaselineSnapshot(),
|
|
self::currentHeavyGovernanceSnapshot(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function heavyGovernanceBudgetOutcome(): array
|
|
{
|
|
$snapshots = self::heavyGovernanceBudgetSnapshots();
|
|
$contract = self::heavyGovernanceBudgetContract();
|
|
$remainingOpenFamilies = array_values(array_map(
|
|
static fn (array $record): string => $record['familyId'],
|
|
array_filter(
|
|
self::heavyGovernanceHotspotInventory(),
|
|
static fn (array $record): bool => in_array($record['status'], ['retained', 'follow-up'], true),
|
|
),
|
|
));
|
|
$followUpDebt = array_values(array_map(
|
|
static fn (array $decision): string => $decision['familyId'],
|
|
array_filter(
|
|
self::heavyGovernanceSlimmingDecisions(),
|
|
static fn (array $decision): bool => in_array($decision['decisionType'], ['retain', 'follow-up'], true),
|
|
),
|
|
));
|
|
|
|
$justification = $contract['decisionStatus'] === 'recalibrated'
|
|
? sprintf(
|
|
'The workflow-heavy hotspot families were narrowed, but the honest lane still retains intentional surface-guard depth and the workspace settings residual helper cost, so the normalized heavy-governance threshold is %.0fs.',
|
|
$contract['normalizedThresholdSeconds'],
|
|
)
|
|
: 'The baseline-profile and findings-heavy families recovered enough duplicated workflow cost for the heavy-governance lane to fit inside the authoritative 300s threshold.';
|
|
|
|
return TestLaneBudget::buildOutcomeRecord(
|
|
contract: $contract,
|
|
baselineSnapshot: $snapshots[0],
|
|
currentSnapshot: $snapshots[1],
|
|
remainingOpenFamilies: $remainingOpenFamilies,
|
|
justification: $justification,
|
|
followUpDebt: $followUpDebt,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function heavyGovernanceAuthorGuidance(): array
|
|
{
|
|
return [
|
|
[
|
|
'ruleId' => 'heavy-family-reuse-before-creation',
|
|
'whenToUse' => 'A new heavy-governance test touches a known baseline, findings, settings, or surface-guard family.',
|
|
'requiredDecision' => 'Decide whether the new test belongs in the existing family before creating a new family id.',
|
|
'antiPattern' => 'Creating a new heavy family for a scenario that only extends an existing trust boundary.',
|
|
'preferredOutcome' => 'Reuse the existing family and update its hotspot or decomposition notes when the trust type stays the same.',
|
|
],
|
|
[
|
|
'ruleId' => 'heavy-family-create-only-for-new-trust',
|
|
'whenToUse' => 'A proposed heavy test introduces a trust boundary that is not already represented in the canonical family inventory.',
|
|
'requiredDecision' => 'Explain the new trust type, hotspot files, and lane rationale before adding the family.',
|
|
'antiPattern' => 'Adding a vague catch-all family without a stable trust description and hotspot inventory entry.',
|
|
'preferredOutcome' => 'Create a new family only when the trust boundary and hotspot ownership are both new and reviewable.',
|
|
],
|
|
[
|
|
'ruleId' => 'split-discovery-workflow-surface-concerns',
|
|
'whenToUse' => 'A test mixes discovery, workflow, and surface-discipline assertions inside one heavy file.',
|
|
'requiredDecision' => 'Separate discovery-trust, workflow-trust, and surface-trust when the same setup is proving unrelated governance rules.',
|
|
'antiPattern' => 'One heavy test that silently becomes a catch-all for unrelated trust types.',
|
|
'preferredOutcome' => 'Keep each heavy family anchored to one dominant trust type, with any unavoidable secondary cost called out explicitly.',
|
|
],
|
|
[
|
|
'ruleId' => 'retain-intentional-heavy-depth-explicitly',
|
|
'whenToUse' => 'A heavy family stays expensive after duplicate work has been removed.',
|
|
'requiredDecision' => 'Record that the family is intentionally heavy and explain the remaining governance breadth.',
|
|
'antiPattern' => 'Continuing to treat intentional heavy coverage as unexplained budget drift.',
|
|
'preferredOutcome' => 'Mark the family as retained with an intentional-depth rationale and validate it with a focused guard suite.',
|
|
],
|
|
[
|
|
'ruleId' => 'record-helper-or-fixture-residuals',
|
|
'whenToUse' => 'A hotspot is dominated by helper, resolver, or fixture cost rather than broad UI trust.',
|
|
'requiredDecision' => 'Record the residual helper or fixture cause instead of pretending the family itself was fully slimmed.',
|
|
'antiPattern' => 'Claiming a family-width win when the remaining cost is actually outside the family boundary.',
|
|
'preferredOutcome' => 'Route the residual helper or fixture cost into follow-up debt while keeping the family inventory honest.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public static function budgetTarget(string $targetType, string $targetId): ?array
|
|
{
|
|
foreach (self::budgetTargets() as $budgetTarget) {
|
|
if ($budgetTarget['targetType'] === $targetType && $budgetTarget['targetId'] === $targetId) {
|
|
return $budgetTarget;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function lane(string $id): array
|
|
{
|
|
foreach (self::lanes() as $lane) {
|
|
if ($lane['id'] === $id) {
|
|
return $lane;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown test lane [%s].', $id));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function lanes(): array
|
|
{
|
|
return [
|
|
[
|
|
'id' => 'fast-feedback',
|
|
'governanceClass' => 'fast',
|
|
'description' => 'Quick representative feedback for normal local edits.',
|
|
'intendedAudience' => 'Contributors working in the default authoring loop.',
|
|
'includedFamilies' => ['unit', 'core-feature-safety', 'ui-light'],
|
|
'excludedFamilies' => ['browser', 'surface-guard', 'discovery-heavy', 'heavy-governance'],
|
|
'ownershipExpectations' => 'Run on normal edits and escalate to confidence or heavy-governance when the touched surface expands.',
|
|
'defaultEntryPoint' => true,
|
|
'parallelMode' => 'required',
|
|
'selectors' => [
|
|
'includeSuites' => ['Unit'],
|
|
'includePaths' => [
|
|
'tests/Feature/Auth',
|
|
'tests/Feature/Authorization',
|
|
'tests/Feature/EntraAdminRoles',
|
|
'tests/Feature/Findings',
|
|
'tests/Feature/Guards',
|
|
'tests/Feature/Monitoring',
|
|
'tests/Feature/Navigation',
|
|
'tests/Feature/Onboarding',
|
|
'tests/Feature/RequiredPermissions',
|
|
'tests/Feature/Tenants',
|
|
'tests/Feature/Workspaces',
|
|
],
|
|
'includeGroups' => [],
|
|
'includeFiles' => [],
|
|
'excludeSuites' => ['Browser'],
|
|
'excludePaths' => [
|
|
'tests/Architecture',
|
|
'tests/Browser',
|
|
'tests/Deprecation',
|
|
'tests/Feature/078',
|
|
'tests/Feature/090',
|
|
'tests/Feature/144',
|
|
'tests/Feature/OpsUx',
|
|
],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => self::disallowedFamilyFilesForLane('fast-feedback'),
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => 200,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'baselineDeltaTargetPercent' => 50,
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'sqlite-memory',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
[
|
|
'id' => 'confidence',
|
|
'governanceClass' => 'confidence',
|
|
'description' => 'Broader pre-merge validation for non-browser feature and integration work.',
|
|
'intendedAudience' => 'Contributors and reviewers preparing a higher-confidence run before merge.',
|
|
'includedFamilies' => ['unit', 'non-browser-feature-integration', 'ui-light', 'ui-workflow'],
|
|
'excludedFamilies' => ['browser', 'surface-guard', 'discovery-heavy', 'heavy-governance'],
|
|
'ownershipExpectations' => 'Run before merge when a change touches multiple feature surfaces or shared infrastructure.',
|
|
'defaultEntryPoint' => false,
|
|
'parallelMode' => 'required',
|
|
'selectors' => [
|
|
'includeSuites' => ['Unit'],
|
|
'includePaths' => ['tests/Feature'],
|
|
'includeGroups' => [],
|
|
'includeFiles' => self::laneHotspotTestFiles('confidence'),
|
|
'excludeSuites' => ['Browser'],
|
|
'excludePaths' => [
|
|
'tests/Architecture',
|
|
'tests/Browser',
|
|
'tests/Deprecation',
|
|
'tests/Feature/078',
|
|
'tests/Feature/090',
|
|
'tests/Feature/144',
|
|
'tests/Feature/OpsUx',
|
|
],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => self::disallowedFamilyFilesForLane('confidence'),
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => 450,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'sqlite-memory',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
[
|
|
'id' => 'browser',
|
|
'governanceClass' => 'heavy',
|
|
'description' => 'Dedicated browser runtime lane for smoke and workflow validation.',
|
|
'intendedAudience' => 'Contributors validating browser-specific behavior and smoke coverage.',
|
|
'includedFamilies' => ['browser'],
|
|
'excludedFamilies' => ['fast-feedback', 'confidence', 'heavy-governance'],
|
|
'ownershipExpectations' => 'Run when a change touches browser behavior or before promoting browser-sensitive work.',
|
|
'scopeBoundaryNote' => 'Browser lane failures keep the named smoke anchors honest; broader browser fallout stays classification-only unless the active feature explicitly owns repair.',
|
|
'defaultEntryPoint' => false,
|
|
'parallelMode' => 'forbidden',
|
|
'selectors' => [
|
|
'includeSuites' => ['Browser'],
|
|
'includePaths' => ['tests/Browser'],
|
|
'includeGroups' => [],
|
|
'includeFiles' => [],
|
|
'excludeSuites' => [],
|
|
'excludePaths' => [],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => [],
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => 150,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'sqlite-memory',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
[
|
|
'id' => 'heavy-governance',
|
|
'governanceClass' => 'heavy',
|
|
'description' => 'Intentionally expensive governance scans, discovery-heavy parity checks, and high-fan-out operational validations.',
|
|
'intendedAudience' => 'Maintainers and reviewers validating the heaviest governance families on purpose.',
|
|
'includedFamilies' => ['architecture-governance', 'ops-ux', 'ui-workflow', 'surface-guard', 'discovery-heavy'],
|
|
'excludedFamilies' => ['browser', 'ui-light'],
|
|
'ownershipExpectations' => 'Run intentionally when touching governance scans, discovery-heavy parity, or broad UI contract families.',
|
|
'scopeBoundaryNote' => 'Heavy-governance output classifies cutover guard drift and broader baseline fallout, but it does not by itself claim full-suite repair ownership.',
|
|
'defaultEntryPoint' => false,
|
|
'parallelMode' => 'optional',
|
|
'selectors' => [
|
|
'includeSuites' => [],
|
|
'includePaths' => [
|
|
'tests/Architecture',
|
|
'tests/Deprecation',
|
|
'tests/Feature/078',
|
|
'tests/Feature/090',
|
|
'tests/Feature/144',
|
|
'tests/Feature/OpsUx',
|
|
],
|
|
'includeGroups' => [],
|
|
'includeFiles' => self::laneHotspotTestFiles('heavy-governance'),
|
|
'excludeSuites' => ['Browser'],
|
|
'excludePaths' => [],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => [],
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => self::recommendedHeavyGovernanceNormalizedThreshold(),
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => self::heavyGovernanceBudgetContract()['lifecycleState'],
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'mixed',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
[
|
|
'id' => 'profiling',
|
|
'governanceClass' => 'support',
|
|
'description' => 'Serial non-browser profiling support run that exposes the slowest tests and family attribution.',
|
|
'intendedAudience' => 'Maintainers investigating slow-test drift before it becomes baseline behavior.',
|
|
'includedFamilies' => ['unit', 'non-browser-feature-integration', 'ui-workflow', 'surface-guard', 'discovery-heavy'],
|
|
'excludedFamilies' => ['browser'],
|
|
'ownershipExpectations' => 'Run intentionally when refreshing baselines or explaining which heavy families own the cost.',
|
|
'defaultEntryPoint' => false,
|
|
'parallelMode' => 'forbidden',
|
|
'selectors' => [
|
|
'includeSuites' => ['Unit'],
|
|
'includePaths' => ['tests/Feature'],
|
|
'includeGroups' => [],
|
|
'includeFiles' => [],
|
|
'excludeSuites' => ['Browser'],
|
|
'excludePaths' => ['tests/Browser'],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => [],
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'profile-top', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => 3000,
|
|
'baselineSource' => 'measured-current-suite',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'sqlite-memory',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
[
|
|
'id' => 'junit',
|
|
'governanceClass' => 'support',
|
|
'description' => 'Machine-readable report run for the confidence-shaped non-browser scope.',
|
|
'intendedAudience' => 'Contributors and reviewers who need durable artifacts instead of ad-hoc terminal output.',
|
|
'includedFamilies' => ['unit', 'non-browser-feature-integration', 'ui-light', 'ui-workflow'],
|
|
'excludedFamilies' => ['browser', 'surface-guard', 'discovery-heavy', 'heavy-governance'],
|
|
'ownershipExpectations' => 'Run when the latest confidence-shaped machine-readable report needs to be shared or attached to follow-up work.',
|
|
'defaultEntryPoint' => false,
|
|
'parallelMode' => 'required',
|
|
'selectors' => [
|
|
'includeSuites' => ['Unit'],
|
|
'includePaths' => ['tests/Feature'],
|
|
'includeGroups' => [],
|
|
'includeFiles' => self::laneHotspotTestFiles('confidence'),
|
|
'excludeSuites' => ['Browser'],
|
|
'excludePaths' => [
|
|
'tests/Architecture',
|
|
'tests/Browser',
|
|
'tests/Deprecation',
|
|
'tests/Feature/078',
|
|
'tests/Feature/090',
|
|
'tests/Feature/144',
|
|
'tests/Feature/OpsUx',
|
|
],
|
|
'excludeGroups' => [],
|
|
'excludeFiles' => self::disallowedFamilyFilesForLane('junit'),
|
|
],
|
|
'artifacts' => ['summary', 'junit-xml', 'budget-report'],
|
|
'budget' => [
|
|
'thresholdSeconds' => 450,
|
|
'baselineSource' => 'measured-lane',
|
|
'enforcement' => 'warn',
|
|
'lifecycleState' => 'documented',
|
|
'reviewCadence' => 'tighten after two stable runs',
|
|
],
|
|
'dbStrategy' => [
|
|
'connectionMode' => 'sqlite-memory',
|
|
'resetStrategy' => 'refresh-database',
|
|
'seedsPolicy' => 'restricted',
|
|
'schemaBaselineCandidate' => false,
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function mainlineBranch(): string
|
|
{
|
|
return self::MAINLINE_BRANCH;
|
|
}
|
|
|
|
public static function ciRunnerLabel(): string
|
|
{
|
|
return self::CI_RUNNER_LABEL;
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function workflowProfiles(): array
|
|
{
|
|
return [
|
|
[
|
|
'workflowId' => 'pr-fast-feedback',
|
|
'filePath' => '.gitea/workflows/test-pr-fast-feedback.yml',
|
|
'triggerClass' => 'pull-request',
|
|
'gitEvents' => ['pull_request'],
|
|
'branchFilters' => [],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => true,
|
|
'scheduleCron' => null,
|
|
'laneBindings' => ['fast-feedback'],
|
|
],
|
|
[
|
|
'workflowId' => 'main-confidence',
|
|
'filePath' => '.gitea/workflows/test-main-confidence.yml',
|
|
'triggerClass' => 'mainline-push',
|
|
'gitEvents' => ['push'],
|
|
'branchFilters' => [self::mainlineBranch()],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => true,
|
|
'scheduleCron' => null,
|
|
'laneBindings' => ['confidence'],
|
|
],
|
|
[
|
|
'workflowId' => 'heavy-governance-manual',
|
|
'filePath' => '.gitea/workflows/test-heavy-governance.yml',
|
|
'triggerClass' => 'manual',
|
|
'gitEvents' => ['workflow_dispatch'],
|
|
'branchFilters' => [],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => false,
|
|
'scheduleCron' => null,
|
|
'laneBindings' => ['heavy-governance'],
|
|
],
|
|
[
|
|
'workflowId' => 'heavy-governance-scheduled',
|
|
'filePath' => '.gitea/workflows/test-heavy-governance.yml',
|
|
'triggerClass' => 'scheduled',
|
|
'gitEvents' => ['schedule'],
|
|
'branchFilters' => [],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => false,
|
|
'scheduleCron' => '17 4 * * 1-5',
|
|
'laneBindings' => ['heavy-governance'],
|
|
],
|
|
[
|
|
'workflowId' => 'browser-manual',
|
|
'filePath' => '.gitea/workflows/test-browser.yml',
|
|
'triggerClass' => 'manual',
|
|
'gitEvents' => ['workflow_dispatch'],
|
|
'branchFilters' => [],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => false,
|
|
'scheduleCron' => null,
|
|
'laneBindings' => ['browser'],
|
|
],
|
|
[
|
|
'workflowId' => 'browser-scheduled',
|
|
'filePath' => '.gitea/workflows/test-browser.yml',
|
|
'triggerClass' => 'scheduled',
|
|
'gitEvents' => ['schedule'],
|
|
'branchFilters' => [],
|
|
'runnerLabel' => self::ciRunnerLabel(),
|
|
'blockingDefault' => false,
|
|
'scheduleCron' => '43 4 * * 1-5',
|
|
'laneBindings' => ['browser'],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function workflowProfile(string $workflowId): array
|
|
{
|
|
foreach (self::workflowProfiles() as $workflowProfile) {
|
|
if ($workflowProfile['workflowId'] === $workflowId) {
|
|
return $workflowProfile;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown workflow profile [%s].', $workflowId));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function workflowProfilesForLane(string $laneId): array
|
|
{
|
|
return array_values(array_filter(
|
|
self::workflowProfiles(),
|
|
static fn (array $workflowProfile): bool => in_array($laneId, $workflowProfile['laneBindings'], true),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function laneBindings(): array
|
|
{
|
|
return array_map(
|
|
static function (array $lane): array {
|
|
$laneId = (string) $lane['id'];
|
|
$artifactContract = self::artifactPublicationContract($laneId);
|
|
|
|
return [
|
|
'laneId' => $laneId,
|
|
'executionWrapper' => 'scripts/platform-test-lane',
|
|
'reportWrapper' => 'scripts/platform-test-report',
|
|
'commandRef' => self::commandRef($laneId),
|
|
'governanceClass' => (string) $lane['governanceClass'],
|
|
'parallelMode' => (string) $lane['parallelMode'],
|
|
'requiredArtifacts' => $artifactContract['requiredFiles'],
|
|
'optionalArtifacts' => $artifactContract['optionalFiles'] ?? [],
|
|
'artifactExportProfile' => sprintf('%s-export', $laneId),
|
|
];
|
|
},
|
|
self::lanes(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function laneBinding(string $laneId): array
|
|
{
|
|
foreach (self::laneBindings() as $laneBinding) {
|
|
if ($laneBinding['laneId'] === $laneId) {
|
|
return $laneBinding;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown lane binding [%s].', $laneId));
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function artifactPublicationContracts(): array
|
|
{
|
|
return array_map(
|
|
static fn (array $lane): array => self::artifactPublicationContract((string) $lane['id']),
|
|
self::lanes(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function artifactPublicationContract(string $laneId): array
|
|
{
|
|
self::lane($laneId);
|
|
|
|
$requiredFiles = ['summary.md', 'budget.json', 'report.json', 'junit.xml', 'trend-history.json'];
|
|
$optionalFiles = $laneId === 'profiling' ? ['profile.txt'] : [];
|
|
$sourcePatterns = array_map(
|
|
static fn (string $artifactFile): string => sprintf('%s-latest.%s', $laneId, $artifactFile),
|
|
array_merge($requiredFiles, $optionalFiles),
|
|
);
|
|
|
|
return [
|
|
'contractId' => sprintf('%s-artifacts', $laneId),
|
|
'laneId' => $laneId,
|
|
'sourceDirectory' => self::artifactDirectory(),
|
|
'sourcePatterns' => $sourcePatterns,
|
|
'requiredFiles' => $requiredFiles,
|
|
'optionalFiles' => $optionalFiles,
|
|
'stagingDirectory' => sprintf('ci-artifacts/%s', $laneId),
|
|
'stagedNamePattern' => '{laneId}.{artifactFile}',
|
|
'uploadGroupName' => sprintf('%s-artifacts', $laneId),
|
|
'retentionClass' => match ($laneId) {
|
|
'fast-feedback' => 'pr-short',
|
|
'confidence', 'junit' => 'mainline-medium',
|
|
default => 'scheduled-medium',
|
|
},
|
|
];
|
|
}
|
|
|
|
public static function laneTrendContractVersion(): string
|
|
{
|
|
return '1.0.0';
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function laneTrendPolicies(): array
|
|
{
|
|
return array_map(
|
|
static fn (array $lane): array => self::laneTrendPolicy((string) $lane['id']),
|
|
self::lanes(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function laneTrendPolicy(string $laneId, ?string $workflowId = null, ?string $triggerClass = null): array
|
|
{
|
|
self::lane($laneId);
|
|
|
|
$policy = TestLaneBudget::recalibrationPolicy($laneId);
|
|
$workflowProfile = null;
|
|
|
|
if ($workflowId !== null && $workflowId !== '') {
|
|
try {
|
|
$workflowProfile = self::workflowProfile($workflowId);
|
|
} catch (InvalidArgumentException) {
|
|
$workflowProfile = null;
|
|
}
|
|
}
|
|
|
|
$workflowProfile ??= self::workflowProfilesForLane($laneId)[0] ?? null;
|
|
$resolvedTriggerClass = $triggerClass
|
|
?? (is_array($workflowProfile) ? (string) ($workflowProfile['triggerClass'] ?? '') : '');
|
|
|
|
return [
|
|
'retentionLimit' => in_array($laneId, ['fast-feedback', 'confidence', 'browser', 'heavy-governance'], true) ? 20 : 10,
|
|
'comparisonWindowSize' => $laneId === 'profiling' ? 4 : 5,
|
|
'minimumComparableSamples' => 3,
|
|
'varianceFloorSeconds' => TestLaneBudget::trendVarianceFloorSeconds($laneId, $resolvedTriggerClass !== '' ? $resolvedTriggerClass : null),
|
|
'nearBudgetHeadroomSeconds' => TestLaneBudget::nearBudgetHeadroomSeconds($laneId),
|
|
'hotspotFamilyLimit' => 5,
|
|
'hotspotFileLimit' => 3,
|
|
'slowestEntryRetention' => 10,
|
|
'recalibrationPolicy' => [
|
|
'baselineRequiresExplicitReview' => (bool) $policy['baselineRequiresExplicitReview'],
|
|
'budgetRequiresExplicitReview' => (bool) $policy['budgetRequiresExplicitReview'],
|
|
'minimumBudgetEvidenceSamples' => (int) $policy['minimumBudgetEvidenceSamples'],
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function laneScopeSignature(string $laneId): string
|
|
{
|
|
$lane = self::lane($laneId);
|
|
$payload = [
|
|
'laneId' => $laneId,
|
|
'governanceClass' => $lane['governanceClass'],
|
|
'parallelMode' => $lane['parallelMode'],
|
|
'includedFamilies' => $lane['includedFamilies'],
|
|
'excludedFamilies' => $lane['excludedFamilies'],
|
|
'selectors' => $lane['selectors'],
|
|
'artifacts' => $lane['artifacts'],
|
|
'budget' => [
|
|
'baselineSource' => $lane['budget']['baselineSource'],
|
|
'thresholdSeconds' => $lane['budget']['thresholdSeconds'],
|
|
],
|
|
'contractVersion' => self::laneTrendContractVersion(),
|
|
];
|
|
|
|
return sha1(json_encode($payload, JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public static function comparisonFingerprintInputs(string $laneId, ?string $workflowId = null, ?string $triggerClass = null): array
|
|
{
|
|
$lane = self::lane($laneId);
|
|
$workflowProfile = null;
|
|
|
|
if ($workflowId !== null && $workflowId !== '') {
|
|
try {
|
|
$workflowProfile = self::workflowProfile($workflowId);
|
|
} catch (InvalidArgumentException) {
|
|
$workflowProfile = null;
|
|
}
|
|
}
|
|
|
|
$workflowProfile ??= self::workflowProfilesForLane($laneId)[0] ?? null;
|
|
|
|
return [
|
|
'laneId' => $laneId,
|
|
'workflowId' => $workflowId
|
|
?? (is_array($workflowProfile) ? (string) ($workflowProfile['workflowId'] ?? '') : sprintf('local-%s', $laneId)),
|
|
'triggerClass' => $triggerClass
|
|
?? (is_array($workflowProfile) ? (string) ($workflowProfile['triggerClass'] ?? '') : 'local'),
|
|
'contractVersion' => self::laneTrendContractVersion(),
|
|
'baselineSource' => (string) ($lane['budget']['baselineSource'] ?? 'measured-lane'),
|
|
'laneScopeSignature' => self::laneScopeSignature($laneId),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public static function failureClasses(): array
|
|
{
|
|
return [
|
|
[
|
|
'failureClassId' => 'test-failure',
|
|
'sourceStep' => 'scripts/platform-test-lane',
|
|
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
|
|
'summaryLabel' => 'Test failure',
|
|
'remediationHint' => 'Inspect the lane output and fix the failing test before rerunning the workflow.',
|
|
],
|
|
[
|
|
'failureClassId' => 'wrapper-failure',
|
|
'sourceStep' => 'scripts/platform-test-lane',
|
|
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
|
|
'summaryLabel' => 'Wrapper or manifest failure',
|
|
'remediationHint' => 'Verify the workflow profile, lane binding, and checked-in wrapper entry points still resolve to the intended lane.',
|
|
],
|
|
[
|
|
'failureClassId' => 'budget-breach',
|
|
'sourceStep' => 'tests/Support/TestLaneBudget.php',
|
|
'blockingOn' => ['pull-request'],
|
|
'summaryLabel' => 'Budget breach',
|
|
'remediationHint' => 'Review the measured runtime against the documented variance allowance, classify broader lane fallout explicitly, and only take on repair when the active feature owns that expanded scope.',
|
|
],
|
|
[
|
|
'failureClassId' => 'artifact-publication-failure',
|
|
'sourceStep' => 'scripts/platform-test-artifacts',
|
|
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
|
|
'summaryLabel' => 'Artifact publication failure',
|
|
'remediationHint' => 'Regenerate the lane report artifacts and confirm the staging helper exported the full summary, budget, report, and JUnit bundle.',
|
|
],
|
|
[
|
|
'failureClassId' => 'infrastructure-failure',
|
|
'sourceStep' => 'gitea-runner',
|
|
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
|
|
'summaryLabel' => 'Infrastructure failure',
|
|
'remediationHint' => 'Check checkout, dependency bootstrap, container startup, or runner health before rerunning the workflow.',
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function failureClass(string $failureClassId): array
|
|
{
|
|
foreach (self::failureClasses() as $failureClass) {
|
|
if ($failureClass['failureClassId'] === $failureClassId) {
|
|
return $failureClass;
|
|
}
|
|
}
|
|
|
|
throw new InvalidArgumentException(sprintf('Unknown failure class [%s].', $failureClassId));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function validateWorkflowExecution(string $workflowId, string $laneId): array
|
|
{
|
|
$workflowProfile = self::workflowProfile($workflowId);
|
|
$unresolvedEntryPoints = [];
|
|
|
|
try {
|
|
self::laneBinding($laneId);
|
|
} catch (InvalidArgumentException) {
|
|
$unresolvedEntryPoints[] = 'lane-binding';
|
|
}
|
|
|
|
if (! array_key_exists($laneId, self::COMMAND_REFS)) {
|
|
$unresolvedEntryPoints[] = 'command-ref';
|
|
}
|
|
|
|
if (! is_file(self::repoRoot().'/scripts/platform-test-lane')) {
|
|
$unresolvedEntryPoints[] = 'lane-runner';
|
|
}
|
|
|
|
if (! is_file(self::repoRoot().'/scripts/platform-test-report')) {
|
|
$unresolvedEntryPoints[] = 'report-runner';
|
|
}
|
|
|
|
if (! is_file(self::repoRoot().DIRECTORY_SEPARATOR.$workflowProfile['filePath'])) {
|
|
$unresolvedEntryPoints[] = 'workflow-file';
|
|
}
|
|
|
|
try {
|
|
self::artifactPublicationContract($laneId);
|
|
} catch (InvalidArgumentException) {
|
|
$unresolvedEntryPoints[] = 'artifact-contract';
|
|
}
|
|
|
|
$workflowLaneMatched = in_array($laneId, $workflowProfile['laneBindings'], true);
|
|
$entryPointResolved = $unresolvedEntryPoints === [];
|
|
$valid = $entryPointResolved && $workflowLaneMatched;
|
|
|
|
return [
|
|
'workflowId' => $workflowId,
|
|
'expectedLaneIds' => $workflowProfile['laneBindings'],
|
|
'executedLaneId' => $laneId,
|
|
'entryPointResolved' => $entryPointResolved,
|
|
'workflowLaneMatched' => $workflowLaneMatched,
|
|
'unresolvedEntryPoints' => $unresolvedEntryPoints,
|
|
'valid' => $valid,
|
|
'primaryFailureClassId' => $valid ? null : 'wrapper-failure',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function currentCiContext(string $laneId): array
|
|
{
|
|
$workflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID') ?: null;
|
|
$triggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS') ?: null;
|
|
|
|
if (! is_string($workflowId) && ! is_string($triggerClass)) {
|
|
return [];
|
|
}
|
|
|
|
$executionValidation = null;
|
|
|
|
if (is_string($workflowId) && $workflowId !== '') {
|
|
try {
|
|
$executionValidation = self::validateWorkflowExecution($workflowId, $laneId);
|
|
$triggerClass ??= self::workflowProfile($workflowId)['triggerClass'];
|
|
} catch (InvalidArgumentException) {
|
|
$executionValidation = [
|
|
'entryPointResolved' => false,
|
|
'workflowLaneMatched' => false,
|
|
'primaryFailureClassId' => 'wrapper-failure',
|
|
'expectedLaneIds' => [],
|
|
'unresolvedEntryPoints' => ['workflow-profile'],
|
|
];
|
|
}
|
|
}
|
|
|
|
return array_filter([
|
|
'workflowId' => is_string($workflowId) && $workflowId !== '' ? $workflowId : null,
|
|
'triggerClass' => is_string($triggerClass) && $triggerClass !== '' ? $triggerClass : null,
|
|
'entryPointResolved' => $executionValidation['entryPointResolved'] ?? true,
|
|
'workflowLaneMatched' => $executionValidation['workflowLaneMatched'] ?? true,
|
|
'primaryFailureClassId' => $executionValidation['primaryFailureClassId'] ?? null,
|
|
'expectedLaneIds' => $executionValidation['expectedLaneIds'] ?? null,
|
|
'unresolvedEntryPoints' => $executionValidation['unresolvedEntryPoints'] ?? null,
|
|
], static fn (mixed $value): bool => $value !== null);
|
|
}
|
|
|
|
public static function commandRef(string $laneId): string
|
|
{
|
|
if (! array_key_exists($laneId, self::COMMAND_REFS)) {
|
|
throw new InvalidArgumentException(sprintf('Unknown lane command reference [%s].', $laneId));
|
|
}
|
|
|
|
return self::COMMAND_REFS[$laneId];
|
|
}
|
|
|
|
public static function artifactDirectory(): string
|
|
{
|
|
return self::ARTIFACT_DIRECTORY;
|
|
}
|
|
|
|
public static function fullSuiteBaselineSeconds(): int
|
|
{
|
|
return self::FULL_SUITE_BASELINE_SECONDS;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, float|int|string>|null
|
|
*/
|
|
public static function comparisonBaseline(string $comparisonProfile, string $laneId): ?array
|
|
{
|
|
$profileBaselines = self::COMPARISON_BASELINES[$comparisonProfile] ?? null;
|
|
|
|
if (! is_array($profileBaselines)) {
|
|
return null;
|
|
}
|
|
|
|
$baseline = $profileBaselines[$laneId] ?? null;
|
|
|
|
return is_array($baseline) ? $baseline : null;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function buildCommand(string $laneId): array
|
|
{
|
|
$lane = self::lane($laneId);
|
|
$commandTargets = self::commandTargets($laneId, $lane);
|
|
$command = ['php', 'vendor/bin/pest', '--colors=never'];
|
|
$configurationPath = $commandTargets !== []
|
|
? self::writeLaneConfiguration($laneId, $commandTargets)
|
|
: null;
|
|
$commandSuites = $commandTargets === []
|
|
? self::commandSuites($lane)
|
|
: [];
|
|
|
|
if (! in_array('profile-top', $lane['artifacts'], true)) {
|
|
$command[] = '--compact';
|
|
}
|
|
|
|
if ($lane['parallelMode'] === 'required') {
|
|
$command[] = '--parallel';
|
|
}
|
|
|
|
if ($configurationPath !== null) {
|
|
$command[] = '--configuration='.$configurationPath;
|
|
$command[] = '--testsuite=Lane';
|
|
} elseif ($commandSuites !== []) {
|
|
$command[] = '--testsuite='.implode(',', $commandSuites);
|
|
}
|
|
|
|
$command[] = '--log-junit='.TestLaneReport::artifactPaths($laneId)['junit'];
|
|
|
|
if (in_array('profile-top', $lane['artifacts'], true)) {
|
|
$command[] = '--profile';
|
|
}
|
|
|
|
if ($commandTargets === []) {
|
|
foreach ($lane['selectors']['includeGroups'] as $group) {
|
|
$command[] = '--group='.$group;
|
|
}
|
|
|
|
foreach ($lane['selectors']['excludeGroups'] as $group) {
|
|
$command[] = '--exclude-group='.$group;
|
|
}
|
|
}
|
|
|
|
return $command;
|
|
}
|
|
|
|
public static function runLane(string $laneId): int
|
|
{
|
|
self::ensureArtifactDirectory();
|
|
|
|
$command = self::buildCommand($laneId);
|
|
$process = new Process($command, self::appRoot());
|
|
$process->setTimeout(null);
|
|
|
|
$capturedOutput = '';
|
|
$startedAt = microtime(true);
|
|
|
|
$process->run(function (string $type, string $buffer) use (&$capturedOutput): void {
|
|
echo $buffer;
|
|
|
|
$capturedOutput .= $buffer;
|
|
});
|
|
|
|
TestLaneReport::finalizeLane(
|
|
$laneId,
|
|
microtime(true) - $startedAt,
|
|
$capturedOutput,
|
|
exitCode: $process->getExitCode() ?? 1,
|
|
);
|
|
|
|
return $process->getExitCode() ?? 1;
|
|
}
|
|
|
|
public static function renderLatestReport(string $laneId, ?string $comparisonProfile = null): int
|
|
{
|
|
$artifactPaths = TestLaneReport::artifactPaths($laneId);
|
|
$reportPath = self::absolutePath($artifactPaths['report']);
|
|
$wallClockSeconds = 0.0;
|
|
|
|
if (is_file($reportPath)) {
|
|
$existingReport = json_decode((string) file_get_contents($reportPath), true);
|
|
$wallClockSeconds = (float) ($existingReport['wallClockSeconds'] ?? 0.0);
|
|
}
|
|
|
|
$parsed = TestLaneReport::parseJUnit(self::absolutePath($artifactPaths['junit']), $laneId);
|
|
$profileOutputPath = self::absolutePath($artifactPaths['profile']);
|
|
|
|
$report = TestLaneReport::buildReport(
|
|
laneId: $laneId,
|
|
wallClockSeconds: $wallClockSeconds,
|
|
slowestEntries: $parsed['slowestEntries'],
|
|
durationsByFile: $parsed['durationsByFile'],
|
|
comparisonProfile: $comparisonProfile,
|
|
);
|
|
|
|
TestLaneReport::writeArtifacts(
|
|
laneId: $laneId,
|
|
report: $report,
|
|
profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null,
|
|
exitCode: 0,
|
|
);
|
|
|
|
echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
public static function discoverFiles(string $laneId): array
|
|
{
|
|
$lane = self::lane($laneId);
|
|
$selectors = $lane['selectors'];
|
|
$files = [];
|
|
|
|
foreach (array_merge(self::suiteTargets($selectors['includeSuites']), $selectors['includePaths'], $selectors['includeFiles']) as $target) {
|
|
foreach (self::discoverTarget($target) as $resolvedFile) {
|
|
if (self::isExcluded($resolvedFile, $selectors, $laneId)) {
|
|
continue;
|
|
}
|
|
|
|
$files[] = $resolvedFile;
|
|
}
|
|
}
|
|
|
|
$files = array_values(array_unique($files));
|
|
sort($files);
|
|
|
|
return $files;
|
|
}
|
|
|
|
public static function absolutePath(string $relativePath): string
|
|
{
|
|
return self::appRoot().DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, ltrim($relativePath, '/'));
|
|
}
|
|
|
|
public static function laneConfigurationPath(string $laneId, ?string $artifactDirectory = null): string
|
|
{
|
|
$directory = trim($artifactDirectory ?? self::artifactDirectory(), '/');
|
|
|
|
return sprintf('%s/%s-lane.phpunit.xml', $directory, $laneId);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $family
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public static function familyForFile(string $filePath, ?array $family = null): ?array
|
|
{
|
|
if (is_array($family)) {
|
|
return self::familyMatchesFile($family, $filePath) ? $family : null;
|
|
}
|
|
|
|
$matchedFamilies = [];
|
|
|
|
foreach (self::families() as $candidate) {
|
|
if (! self::familyMatchesFile($candidate, $filePath)) {
|
|
continue;
|
|
}
|
|
|
|
$matchedFamilies[] = [
|
|
'family' => $candidate,
|
|
'score' => self::familyMatchScore($candidate, $filePath),
|
|
];
|
|
}
|
|
|
|
if ($matchedFamilies === []) {
|
|
return null;
|
|
}
|
|
|
|
usort($matchedFamilies, static fn (array $left, array $right): int => $right['score'] <=> $left['score']);
|
|
|
|
return $matchedFamilies[0]['family'];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function validateLanePlacement(
|
|
string $laneId,
|
|
?string $classificationId = null,
|
|
?string $familyId = null,
|
|
?string $filePath = null,
|
|
array $observedCostSignals = [],
|
|
): array {
|
|
$family = is_string($familyId) ? self::family($familyId) : null;
|
|
|
|
if ($family === null && is_string($filePath)) {
|
|
$family = self::familyForFile($filePath);
|
|
}
|
|
|
|
$mixedResolution = is_string($filePath) ? self::mixedFileResolution($filePath) : null;
|
|
$resolvedClassificationId = $classificationId
|
|
?? ($mixedResolution['primaryClassificationId'] ?? null)
|
|
?? ($family['classificationId'] ?? null);
|
|
|
|
if (! is_string($resolvedClassificationId) || $resolvedClassificationId === '') {
|
|
throw new InvalidArgumentException('Lane placement validation requires a classification, family, or resolvable file path.');
|
|
}
|
|
|
|
$classification = self::classification($resolvedClassificationId);
|
|
$rule = self::placementRuleFor($resolvedClassificationId, $laneId);
|
|
|
|
$allowance = $rule['allowance']
|
|
?? (in_array($laneId, $classification['allowedLaneIds'], true)
|
|
? 'allowed'
|
|
: (in_array($laneId, $classification['forbiddenLaneIds'], true)
|
|
? 'forbidden'
|
|
: ($classification['defaultLaneId'] === $laneId ? 'required' : 'discouraged')));
|
|
|
|
$reasons = [];
|
|
|
|
if (is_array($rule)) {
|
|
$reasons[] = (string) $rule['reason'];
|
|
}
|
|
|
|
if (is_array($family)) {
|
|
$reasons[] = sprintf(
|
|
'Family [%s] is cataloged as [%s] and targets lane [%s].',
|
|
$family['familyId'],
|
|
$family['classificationId'],
|
|
$family['targetLaneId'],
|
|
);
|
|
}
|
|
|
|
if (is_array($mixedResolution)) {
|
|
$reasons[] = sprintf(
|
|
'Mixed-file resolution [%s] applies [%s] via [%s].',
|
|
$mixedResolution['filePath'],
|
|
$mixedResolution['primaryClassificationId'],
|
|
$mixedResolution['resolutionStrategy'],
|
|
);
|
|
}
|
|
|
|
if ($observedCostSignals !== []) {
|
|
$reasons[] = sprintf('Observed cost signals: %s.', implode(', ', $observedCostSignals));
|
|
}
|
|
|
|
$valid = $allowance !== 'forbidden';
|
|
|
|
if (is_array($family) && ! in_array($laneId, self::allowedFamilyTargetLanesForLane($laneId), true)) {
|
|
$valid = false;
|
|
}
|
|
|
|
if (is_array($family) && ! in_array($family['targetLaneId'], self::allowedFamilyTargetLanesForLane($laneId), true)) {
|
|
$allowance = 'forbidden';
|
|
$valid = false;
|
|
$reasons[] = sprintf(
|
|
'Family [%s] targets [%s], which is outside the allowed family targets for lane [%s].',
|
|
$family['familyId'],
|
|
$family['targetLaneId'],
|
|
$laneId,
|
|
);
|
|
}
|
|
|
|
$remediationOptions = [];
|
|
|
|
if (! $valid) {
|
|
$remediationOptions[] = sprintf(
|
|
'Move the file or family into [%s].',
|
|
is_array($family) ? $family['targetLaneId'] : $classification['defaultLaneId'],
|
|
);
|
|
$remediationOptions[] = sprintf('Tag the surface with [%s] or [%s] groups as appropriate.', $resolvedClassificationId, is_array($family) ? $family['targetLaneId'] : $classification['defaultLaneId']);
|
|
}
|
|
|
|
return array_filter([
|
|
'laneId' => $laneId,
|
|
'resolvedClassificationId' => $resolvedClassificationId,
|
|
'familyId' => $family['familyId'] ?? null,
|
|
'allowance' => $allowance,
|
|
'valid' => $valid,
|
|
'reasons' => $reasons,
|
|
'remediationOptions' => $remediationOptions,
|
|
'mixedFileResolution' => $mixedResolution,
|
|
], static fn (mixed $value): bool => $value !== null && $value !== []);
|
|
}
|
|
|
|
public static function describeFilePlacement(string $filePath): string
|
|
{
|
|
$family = self::familyForFile($filePath);
|
|
|
|
if ($family === null) {
|
|
return sprintf('File [%s] is not cataloged in the Spec 208 family inventory.', $filePath);
|
|
}
|
|
|
|
$mixedResolution = self::mixedFileResolution($filePath);
|
|
|
|
$message = sprintf(
|
|
'File [%s] resolves to family [%s] in class [%s] targeting lane [%s].',
|
|
$filePath,
|
|
$family['familyId'],
|
|
$family['classificationId'],
|
|
$family['targetLaneId'],
|
|
);
|
|
|
|
if (is_array($mixedResolution)) {
|
|
$message .= sprintf(
|
|
' Mixed-file strategy [%s] keeps [%s] as the primary class.',
|
|
$mixedResolution['resolutionStrategy'],
|
|
$mixedResolution['primaryClassificationId'],
|
|
);
|
|
}
|
|
|
|
return $message;
|
|
}
|
|
|
|
private static function heavyGovernanceSummaryThresholdSeconds(): float
|
|
{
|
|
return 300.0;
|
|
}
|
|
|
|
private static function heavyGovernanceLegacyEvaluationThresholdSeconds(): float
|
|
{
|
|
return 200.0;
|
|
}
|
|
|
|
private static function recommendedHeavyGovernanceNormalizedThreshold(?float $measuredSeconds = null): float
|
|
{
|
|
$resolvedMeasuredSeconds = round($measuredSeconds ?? self::heavyGovernanceCurrentMeasuredSeconds(), 6);
|
|
|
|
if ($resolvedMeasuredSeconds <= self::heavyGovernanceSummaryThresholdSeconds()) {
|
|
return self::heavyGovernanceSummaryThresholdSeconds();
|
|
}
|
|
|
|
return round((float) (ceil($resolvedMeasuredSeconds / 5.0) * 5.0), 6);
|
|
}
|
|
|
|
private static function heavyGovernanceCurrentMeasuredSeconds(): float
|
|
{
|
|
$report = self::readJsonArtifact(self::heavyGovernanceArtifactPaths('latest')['report']);
|
|
|
|
if (is_array($report)) {
|
|
return round((float) ($report['wallClockSeconds'] ?? 0.0), 6);
|
|
}
|
|
|
|
return round((float) self::seededHeavyGovernanceBaselineSnapshot()['wallClockSeconds'], 6);
|
|
}
|
|
|
|
/**
|
|
* @return array{summary: string, budget: string, report: string}
|
|
*/
|
|
private static function heavyGovernanceArtifactPaths(string $variant): array
|
|
{
|
|
$directory = self::artifactDirectory();
|
|
$suffix = $variant === 'baseline' ? 'baseline' : 'latest';
|
|
|
|
return [
|
|
'summary' => sprintf('%s/heavy-governance-%s.summary.md', $directory, $suffix),
|
|
'budget' => sprintf('%s/heavy-governance-%s.budget.json', $directory, $suffix),
|
|
'report' => sprintf('%s/heavy-governance-%s.report.json', $directory, $suffix),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private static function readJsonArtifact(string $relativePath): ?array
|
|
{
|
|
$absolutePath = self::absolutePath($relativePath);
|
|
|
|
if (! is_file($absolutePath)) {
|
|
return null;
|
|
}
|
|
|
|
$decoded = json_decode((string) file_get_contents($absolutePath), true);
|
|
|
|
return is_array($decoded) ? $decoded : null;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, float>
|
|
*/
|
|
private static function heavyGovernanceBaselineMeasuredSecondsByFamily(): array
|
|
{
|
|
$snapshot = self::seededHeavyGovernanceBaselineSnapshot();
|
|
$totals = [];
|
|
|
|
foreach ($snapshot['familyTotals'] as $entry) {
|
|
$totals[(string) $entry['familyId']] = round((float) $entry['totalWallClockSeconds'], 6);
|
|
}
|
|
|
|
return $totals;
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function currentHeavyGovernanceSnapshot(): array
|
|
{
|
|
$fallback = self::seededHeavyGovernanceBaselineSnapshot();
|
|
$report = self::readJsonArtifact(self::heavyGovernanceArtifactPaths('latest')['report']);
|
|
$measuredSeconds = round((float) ($report['wallClockSeconds'] ?? $fallback['wallClockSeconds']), 6);
|
|
$normalizedThresholdSeconds = self::recommendedHeavyGovernanceNormalizedThreshold($measuredSeconds);
|
|
|
|
if (! is_array($report)) {
|
|
return array_merge($fallback, [
|
|
'snapshotId' => 'post-slimming',
|
|
'artifactPaths' => self::heavyGovernanceArtifactPaths('latest'),
|
|
'budgetStatus' => $measuredSeconds <= $normalizedThresholdSeconds ? 'within-budget' : 'warning',
|
|
]);
|
|
}
|
|
|
|
return [
|
|
'snapshotId' => 'post-slimming',
|
|
'capturedAt' => (string) ($report['finishedAt'] ?? gmdate('c')),
|
|
'wallClockSeconds' => $measuredSeconds,
|
|
'classificationTotals' => array_map(
|
|
static fn (array $entry): array => [
|
|
'classificationId' => (string) $entry['classificationId'],
|
|
'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6),
|
|
],
|
|
$report['classificationAttribution'] ?? [],
|
|
),
|
|
'familyTotals' => array_map(
|
|
static fn (array $entry): array => [
|
|
'familyId' => (string) $entry['familyId'],
|
|
'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6),
|
|
],
|
|
$report['familyAttribution'] ?? [],
|
|
),
|
|
'slowestEntries' => array_map(
|
|
static fn (array $entry): array => [
|
|
'label' => (string) $entry['label'],
|
|
'wallClockSeconds' => round((float) ($entry['wallClockSeconds'] ?? 0.0), 6),
|
|
],
|
|
$report['slowestEntries'] ?? [],
|
|
),
|
|
'artifactPaths' => self::heavyGovernanceArtifactPaths('latest'),
|
|
'budgetStatus' => $measuredSeconds <= $normalizedThresholdSeconds ? 'within-budget' : 'warning',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private static function seededHeavyGovernanceBaselineSnapshot(): array
|
|
{
|
|
return [
|
|
'snapshotId' => 'pre-slimming',
|
|
'capturedAt' => '2026-04-17T11:00:53+00:00',
|
|
'wallClockSeconds' => 318.296962,
|
|
'classificationTotals' => [
|
|
['classificationId' => 'ui-workflow', 'totalWallClockSeconds' => 190.606431],
|
|
['classificationId' => 'surface-guard', 'totalWallClockSeconds' => 106.845887],
|
|
['classificationId' => 'discovery-heavy', 'totalWallClockSeconds' => 0.863003],
|
|
],
|
|
'familyTotals' => [
|
|
['familyId' => 'baseline-profile-start-surfaces', 'totalWallClockSeconds' => 98.112193],
|
|
['familyId' => 'action-surface-contract', 'totalWallClockSeconds' => 40.841552],
|
|
['familyId' => 'ops-ux-governance', 'totalWallClockSeconds' => 38.794861],
|
|
['familyId' => 'findings-workflow-surfaces', 'totalWallClockSeconds' => 36.459493],
|
|
['familyId' => 'finding-bulk-actions-workflow', 'totalWallClockSeconds' => 26.491446],
|
|
['familyId' => 'workspace-settings-slice-management', 'totalWallClockSeconds' => 21.740839],
|
|
['familyId' => 'workspace-only-admin-surface-independence', 'totalWallClockSeconds' => 11.639077],
|
|
['familyId' => 'panel-navigation-segregation', 'totalWallClockSeconds' => 11.022529],
|
|
['familyId' => 'drift-bulk-triage-all-matching', 'totalWallClockSeconds' => 7.80246],
|
|
['familyId' => 'backup-items-relation-manager-enforcement', 'totalWallClockSeconds' => 2.280078],
|
|
['familyId' => 'environment-review-header-discipline', 'totalWallClockSeconds' => 1.257656],
|
|
['familyId' => 'workspace-memberships-relation-manager-enforcement', 'totalWallClockSeconds' => 1.010134],
|
|
['familyId' => 'policy-resource-admin-search-parity', 'totalWallClockSeconds' => 0.439257],
|
|
['familyId' => 'policy-version-admin-search-parity', 'totalWallClockSeconds' => 0.423746],
|
|
],
|
|
'slowestEntries' => [
|
|
['label' => 'tests/Feature/Findings/FindingBulkActionsTest.php::it supports bulk workflow actions and audits each record', 'wallClockSeconds' => 26.491446],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it starts capture successfully for authorized workspace members', 'wallClockSeconds' => 12.373413],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it starts baseline compare successfully for authorized workspace members', 'wallClockSeconds' => 12.228384],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it does not start full-content baseline compare when rollout is disabled', 'wallClockSeconds' => 11.204111],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it does not start full-content capture when rollout is disabled', 'wallClockSeconds' => 11.086623],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it does not start baseline compare for workspace members missing tenant.sync', 'wallClockSeconds' => 10.659623],
|
|
['label' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php::it keeps baseline capture and compare actions capability-gated on the profile detail page', 'wallClockSeconds' => 10.555709],
|
|
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it does not start capture for workspace members missing workspace_baselines.manage', 'wallClockSeconds' => 10.428982],
|
|
['label' => 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php::triage all matching requires typed confirmation when triaging more than 100 findings', 'wallClockSeconds' => 7.80246],
|
|
['label' => 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php::it keeps workspace-only admin surfaces independent from remembered environment changes', 'wallClockSeconds' => 7.779388],
|
|
],
|
|
'artifactPaths' => self::heavyGovernanceArtifactPaths('baseline'),
|
|
'budgetStatus' => 'warning',
|
|
];
|
|
}
|
|
|
|
private static function appRoot(): string
|
|
{
|
|
return dirname(__DIR__, 2);
|
|
}
|
|
|
|
public static function repoRoot(): string
|
|
{
|
|
$configuredRoot = getenv('TENANTATLAS_REPO_ROOT');
|
|
|
|
if (is_string($configuredRoot) && $configuredRoot !== '') {
|
|
if (str_starts_with($configuredRoot, DIRECTORY_SEPARATOR)) {
|
|
return rtrim($configuredRoot, DIRECTORY_SEPARATOR);
|
|
}
|
|
|
|
$resolvedConfiguredRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.$configuredRoot);
|
|
|
|
if ($resolvedConfiguredRoot !== false) {
|
|
return $resolvedConfiguredRoot;
|
|
}
|
|
}
|
|
|
|
$resolvedDefaultRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.'../..');
|
|
|
|
if ($resolvedDefaultRoot !== false) {
|
|
return $resolvedDefaultRoot;
|
|
}
|
|
|
|
return dirname(dirname(self::appRoot()));
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $targets
|
|
*/
|
|
private static function writeLaneConfiguration(string $laneId, array $targets): string
|
|
{
|
|
$relativePath = self::laneConfigurationPath($laneId);
|
|
$absolutePath = self::absolutePath($relativePath);
|
|
|
|
self::ensureDirectory(dirname($absolutePath));
|
|
|
|
$xml = new \DOMDocument('1.0', 'UTF-8');
|
|
$xml->preserveWhiteSpace = false;
|
|
$xml->formatOutput = true;
|
|
$xml->load(self::absolutePath('phpunit.xml'));
|
|
|
|
$phpunit = $xml->documentElement;
|
|
|
|
if (! $phpunit instanceof \DOMElement) {
|
|
throw new \RuntimeException('Unable to load phpunit.xml root element.');
|
|
}
|
|
|
|
$phpunit->setAttribute('bootstrap', self::absolutePath('vendor/autoload.php'));
|
|
|
|
foreach (iterator_to_array($phpunit->childNodes) as $childNode) {
|
|
if ($childNode instanceof \DOMElement && $childNode->tagName === 'testsuites') {
|
|
$phpunit->removeChild($childNode);
|
|
}
|
|
}
|
|
|
|
foreach (iterator_to_array($phpunit->getElementsByTagName('directory')) as $directoryNode) {
|
|
if (trim($directoryNode->textContent) === 'app') {
|
|
$directoryNode->nodeValue = self::absolutePath('app');
|
|
}
|
|
}
|
|
|
|
$testsuites = $xml->createElement('testsuites');
|
|
$testsuite = $xml->createElement('testsuite');
|
|
$testsuite->setAttribute('name', 'Lane');
|
|
|
|
foreach ($targets as $target) {
|
|
$testsuite->appendChild($xml->createElement('file', self::absolutePath($target)));
|
|
}
|
|
|
|
$testsuites->appendChild($testsuite);
|
|
|
|
$insertBefore = null;
|
|
|
|
foreach (iterator_to_array($phpunit->childNodes) as $childNode) {
|
|
if ($childNode instanceof \DOMElement && in_array($childNode->tagName, ['source', 'php'], true)) {
|
|
$insertBefore = $childNode;
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($insertBefore instanceof \DOMElement) {
|
|
$phpunit->insertBefore($testsuites, $insertBefore);
|
|
} else {
|
|
$phpunit->appendChild($testsuites);
|
|
}
|
|
|
|
$xml->save($absolutePath);
|
|
|
|
return $relativePath;
|
|
}
|
|
|
|
private static function ensureArtifactDirectory(): void
|
|
{
|
|
$directory = self::absolutePath(self::artifactDirectory());
|
|
|
|
if (is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
mkdir($directory, 0777, true);
|
|
}
|
|
|
|
private static function ensureDirectory(string $directory): void
|
|
{
|
|
if (is_dir($directory)) {
|
|
return;
|
|
}
|
|
|
|
mkdir($directory, 0777, true);
|
|
}
|
|
|
|
/**
|
|
* @param list<string> $suites
|
|
* @return list<string>
|
|
*/
|
|
private static function suiteTargets(array $suites): array
|
|
{
|
|
$targets = [];
|
|
|
|
foreach ($suites as $suite) {
|
|
$targets = array_merge($targets, match ($suite) {
|
|
'Unit' => ['tests/Unit'],
|
|
'Feature' => ['tests/Feature'],
|
|
'Browser' => ['tests/Browser'],
|
|
'Pgsql' => ['tests/Feature/WorkspaceIsolation/WorkspaceIdForeignKeyConstraintTest.php'],
|
|
default => [],
|
|
});
|
|
}
|
|
|
|
return array_values(array_unique($targets));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $lane
|
|
* @return list<string>
|
|
*/
|
|
private static function commandTargets(string $laneId, array $lane): array
|
|
{
|
|
$targets = self::discoverFiles($laneId);
|
|
|
|
if ($targets !== []) {
|
|
return $targets;
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $lane
|
|
* @return list<string>
|
|
*/
|
|
private static function commandSuites(array $lane): array
|
|
{
|
|
$selectors = $lane['selectors'];
|
|
$suites = [];
|
|
|
|
foreach ($selectors['includeSuites'] as $suite) {
|
|
$suites[] = $suite;
|
|
}
|
|
|
|
foreach (array_merge($selectors['includePaths'], $selectors['includeFiles']) as $target) {
|
|
$suite = self::inferSuiteFromTarget($target);
|
|
|
|
if ($suite !== null) {
|
|
$suites[] = $suite;
|
|
}
|
|
}
|
|
|
|
$suites = array_values(array_unique(array_filter($suites, static fn (mixed $suite): bool => is_string($suite) && $suite !== '')));
|
|
|
|
if ($selectors['excludeSuites'] !== []) {
|
|
$suites = array_values(array_filter(
|
|
$suites,
|
|
static fn (string $suite): bool => ! in_array($suite, $selectors['excludeSuites'], true),
|
|
));
|
|
}
|
|
|
|
$suiteOrder = [
|
|
'Unit' => 10,
|
|
'Feature' => 20,
|
|
'Browser' => 30,
|
|
'Architecture' => 40,
|
|
'Deprecation' => 50,
|
|
'Pgsql' => 60,
|
|
];
|
|
|
|
usort($suites, static function (string $left, string $right) use ($suiteOrder): int {
|
|
return ($suiteOrder[$left] ?? 999) <=> ($suiteOrder[$right] ?? 999);
|
|
});
|
|
|
|
return $suites;
|
|
}
|
|
|
|
private static function inferSuiteFromTarget(string $target): ?string
|
|
{
|
|
return match (true) {
|
|
str_starts_with($target, 'tests/Unit') => 'Unit',
|
|
str_starts_with($target, 'tests/Feature') => 'Feature',
|
|
str_starts_with($target, 'tests/Browser') => 'Browser',
|
|
str_starts_with($target, 'tests/Architecture') => 'Architecture',
|
|
str_starts_with($target, 'tests/Deprecation') => 'Deprecation',
|
|
default => null,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function discoverTarget(string $target): array
|
|
{
|
|
$absoluteTarget = self::absolutePath($target);
|
|
|
|
if (is_file($absoluteTarget)) {
|
|
return [str_replace(DIRECTORY_SEPARATOR, '/', $target)];
|
|
}
|
|
|
|
if (! is_dir($absoluteTarget)) {
|
|
return [];
|
|
}
|
|
|
|
$files = [];
|
|
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absoluteTarget));
|
|
|
|
/** @var SplFileInfo $file */
|
|
foreach ($iterator as $file) {
|
|
if (! $file->isFile() || ! str_ends_with($file->getFilename(), 'Test.php')) {
|
|
continue;
|
|
}
|
|
|
|
$resolvedPath = str_replace(self::appRoot().DIRECTORY_SEPARATOR, '', $file->getPathname());
|
|
$files[] = str_replace(DIRECTORY_SEPARATOR, '/', $resolvedPath);
|
|
}
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $selectors
|
|
*/
|
|
private static function isExcluded(string $filePath, array $selectors, string $laneId): bool
|
|
{
|
|
if (in_array($filePath, $selectors['excludeFiles'], true)) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($selectors['excludePaths'] as $excludedPath) {
|
|
if (str_starts_with($filePath, rtrim($excludedPath, '/'))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
$family = self::familyForFile($filePath);
|
|
|
|
if ($family === null) {
|
|
return false;
|
|
}
|
|
|
|
return ! in_array($family['targetLaneId'], self::allowedFamilyTargetLanesForLane($laneId), true);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private static function placementRuleFor(string $classificationId, string $laneId): ?array
|
|
{
|
|
foreach (self::placementRules() as $rule) {
|
|
if ($rule['classificationId'] === $classificationId && $rule['laneId'] === $laneId) {
|
|
return $rule;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function allowedFamilyTargetLanesForLane(string $laneId): array
|
|
{
|
|
return match ($laneId) {
|
|
'fast-feedback' => ['fast-feedback'],
|
|
'confidence', 'junit' => ['fast-feedback', 'confidence'],
|
|
'heavy-governance' => ['heavy-governance'],
|
|
'browser' => ['browser'],
|
|
'profiling' => ['fast-feedback', 'confidence', 'heavy-governance'],
|
|
default => [$laneId],
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function disallowedFamilyFilesForLane(string $laneId): array
|
|
{
|
|
$allowedTargets = self::allowedFamilyTargetLanesForLane($laneId);
|
|
$files = [];
|
|
|
|
foreach (self::families() as $family) {
|
|
if (in_array($family['targetLaneId'], $allowedTargets, true)) {
|
|
continue;
|
|
}
|
|
|
|
$files = array_merge($files, self::familyHotspotTestFiles($family));
|
|
}
|
|
|
|
$files = array_values(array_unique($files));
|
|
sort($files);
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* @return list<string>
|
|
*/
|
|
private static function laneHotspotTestFiles(string $laneId): array
|
|
{
|
|
$files = [];
|
|
|
|
foreach (self::familiesByTargetLane($laneId) as $family) {
|
|
$files = array_merge($files, self::familyHotspotTestFiles($family));
|
|
}
|
|
|
|
$files = array_values(array_unique($files));
|
|
sort($files);
|
|
|
|
return $files;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
* @return list<string>
|
|
*/
|
|
private static function familyHotspotTestFiles(array $family): array
|
|
{
|
|
return array_values(array_filter(
|
|
$family['hotspotFiles'],
|
|
static fn (mixed $filePath): bool => is_string($filePath) && str_ends_with($filePath, 'Test.php'),
|
|
));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
*/
|
|
private static function primaryBudgetSelectorType(array $family): string
|
|
{
|
|
foreach ($family['selectors'] as $selector) {
|
|
if (($selector['selectorType'] ?? null) === 'path' && in_array($selector['selectorRole'] ?? 'include', ['include', 'inventory-only'], true)) {
|
|
return 'path';
|
|
}
|
|
}
|
|
|
|
return 'file';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
* @return list<string>
|
|
*/
|
|
private static function budgetSelectorsForFamily(array $family): array
|
|
{
|
|
$selectors = [];
|
|
|
|
foreach ($family['selectors'] as $selector) {
|
|
if (! in_array($selector['selectorRole'] ?? 'include', ['include', 'inventory-only'], true)) {
|
|
continue;
|
|
}
|
|
|
|
if (! in_array($selector['selectorType'] ?? 'file', ['file', 'path'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$selectors[] = (string) $selector['selectorValue'];
|
|
}
|
|
|
|
return array_values(array_unique($selectors));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
*/
|
|
private static function familyMatchesFile(array $family, string $filePath): bool
|
|
{
|
|
if (in_array($filePath, $family['hotspotFiles'], true)) {
|
|
return true;
|
|
}
|
|
|
|
foreach ($family['selectors'] as $selector) {
|
|
if (! in_array($selector['selectorRole'] ?? 'include', ['include', 'inventory-only'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$selectorType = (string) ($selector['selectorType'] ?? 'file');
|
|
$selectorValue = (string) ($selector['selectorValue'] ?? '');
|
|
|
|
if ($selectorValue === '') {
|
|
continue;
|
|
}
|
|
|
|
$matches = match ($selectorType) {
|
|
'file' => $filePath === $selectorValue,
|
|
'path' => str_starts_with($filePath, rtrim($selectorValue, '/')),
|
|
default => false,
|
|
};
|
|
|
|
if ($matches) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $family
|
|
*/
|
|
private static function familyMatchScore(array $family, string $filePath): int
|
|
{
|
|
$score = 0;
|
|
|
|
foreach ($family['selectors'] as $selector) {
|
|
if (! in_array($selector['selectorRole'] ?? 'include', ['include', 'inventory-only'], true)) {
|
|
continue;
|
|
}
|
|
|
|
$selectorType = (string) ($selector['selectorType'] ?? 'file');
|
|
$selectorValue = (string) ($selector['selectorValue'] ?? '');
|
|
|
|
if ($selectorType === 'file' && $selectorValue === $filePath) {
|
|
$score = max($score, 2_000 + strlen($selectorValue));
|
|
}
|
|
|
|
if ($selectorType === 'path' && str_starts_with($filePath, rtrim($selectorValue, '/'))) {
|
|
$score = max($score, 1_000 + strlen($selectorValue));
|
|
}
|
|
}
|
|
|
|
if (in_array($filePath, $family['hotspotFiles'], true)) {
|
|
$score = max($score, 1_500 + strlen($filePath));
|
|
}
|
|
|
|
return $score;
|
|
}
|
|
}
|