TenantAtlas/apps/platform/tests/Support/TestLaneManifest.php
ahmido 0d5d1fc9f4 Spec 208: finalize heavy suite segmentation (#241)
## Summary
- add the checked-in Spec 208 heavy-suite classification and family manifest with config-driven lane generation, attribution, and budget reporting
- update Pest grouping, guard coverage, wrapper/report contracts, and spec artifacts for the segmented lane model
- complete the targeted follow-up pass that re-homes the remaining in-scope confidence hotspots into explicit heavy-governance families

## Acceptance
- confidence is repaired and now measures 389.613832s, down from 587.446894s and below the 450s lane budget
- confidence is also slightly below the post-Spec-207 baseline of 394.383441s (delta -4.769609s)
- this closes the central Spec 208 acceptance issue that had kept the spec open

## Intentionally Re-homed Families
- finding-bulk-actions-workflow
- drift-bulk-triage-all-matching
- baseline-profile-start-surfaces
- workspace-settings-slice-management
- findings-workflow-surfaces
- workspace-only-admin-surface-independence

## Explicit Residual Risk
- heavy-governance now measures 318.296962s, above its documented 300s threshold
- the cost was not removed; it was moved into the correct lane and made visible on clearly named heavy families
- this is documented residual debt, not an open Spec 208 failure

## Validation
- focused guard/support validation: 206 passed (3607 assertions)
- lane wrapper/report validation completed for confidence and heavy-governance
- no full-suite run was performed in this pass by request

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #241
2026-04-17 09:53:55 +00:00

2322 lines
100 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 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' => 1,
'artifactDirectory' => self::artifactDirectory(),
'classifications' => self::classifications(),
'families' => self::families(),
'mixedFileResolutions' => self::mixedFileResolutions(),
'placementRules' => self::placementRules(),
'driftGuards' => self::driftGuards(),
'budgetTargets' => self::budgetTargets(),
'lanes' => self::lanes(),
'familyBudgets' => self::familyBudgets(),
];
}
/**
* @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 tenant 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 tenant 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 tenant 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 tenant 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' => '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' => 'tenant-review-header-discipline',
'classificationId' => 'surface-guard',
'purpose' => 'Enforce primary and grouped header action discipline for tenant 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/TenantReviewHeaderDisciplineTest.php',
'selectorRole' => 'inventory-only',
'sourceOfTruth' => 'manifest',
'rationale' => 'Seeded header discipline hotspot.',
],
],
'hotspotFiles' => [
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.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' => '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.',
],
],
'hotspotFiles' => [
'tests/Browser/Spec190BaselineCompareMatrixSmokeTest.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 tenant 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' => 200,
'baselineSource' => 'measured-lane',
'enforcement' => 'warn',
'lifecycleState' => 'documented',
'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-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-tenant-review-header-discipline',
'targetType' => 'family',
'targetId' => 'tenant-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>|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.',
'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.',
'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' => 300,
'baselineSource' => 'measured-lane',
'enforcement' => 'warn',
'lifecycleState' => 'documented',
'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 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);
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,
);
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 appRoot(): string
{
return dirname(__DIR__, 2);
}
/**
* @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;
}
}