Compare commits

...

3 Commits

Author SHA1 Message Date
bf38ec1780 Spec 210: implement CI test matrix budget enforcement (#243)
Some checks failed
Main Confidence / confidence (push) Failing after 3m36s
## Summary
- add explicit Gitea workflow files for PR Fast Feedback, `dev` Confidence, Heavy Governance, and Browser lanes
- extend the repo-truth lane support seams with workflow profiles, trigger-aware budget enforcement, artifact publication contracts, CI summaries, and failure classification
- add deterministic artifact staging, new CI governance guard coverage, and Spec 210 planning/contracts/docs updates

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php tests/Feature/Guards/CiConfidenceWorkflowContractTest.php tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php tests/Feature/Guards/CiLaneFailureClassificationContractTest.php tests/Feature/Guards/FastFeedbackLaneContractTest.php tests/Feature/Guards/ConfidenceLaneContractTest.php tests/Feature/Guards/HeavyGovernanceLaneContractTest.php tests/Feature/Guards/BrowserLaneIsolationTest.php tests/Feature/Guards/FixtureLaneImpactBudgetTest.php tests/Feature/Guards/TestLaneManifestTest.php tests/Feature/Guards/TestLaneArtifactsContractTest.php tests/Feature/Guards/TestLaneCommandContractTest.php`
- `./scripts/platform-test-lane fast-feedback`
- `./scripts/platform-test-lane confidence`
- `./scripts/platform-test-lane heavy-governance`
- `./scripts/platform-test-lane browser`
- `./scripts/platform-test-report fast-feedback`
- `./scripts/platform-test-report confidence`

## Notes
- scheduled Heavy Governance and Browser workflows stay gated behind `TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE=1` and `TENANTATLAS_ENABLE_BROWSER_SCHEDULE=1`
- the remaining rollout evidence task is capturing the live Gitea run set this PR enables: PR Fast Feedback, `dev` Confidence, manual and scheduled Heavy Governance, and manual and scheduled Browser runs

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #243
2026-04-17 18:04:35 +00:00
a2fdca43fd feat: implement heavy governance cost recovery (#242)
## Summary
- implement Spec 209 heavy-governance cost recovery end to end
- add the heavy-governance contract, hotspot inventory, decomposition, snapshots, budget outcome, and author-guidance surfaces in the shared lane support seams
- slim the baseline and findings hotspot families, harden wrapper behavior, and refresh the spec, quickstart, and contract artifacts

## Validation
- `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/TestLaneCommandContractTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Filament/BaselineActionAuthorizationTest.php tests/Feature/Findings/FindingsListFiltersTest.php tests/Feature/Findings/FindingExceptionRenewalTest.php tests/Feature/Findings/FindingWorkflowRowActionsTest.php tests/Feature/Findings/FindingWorkflowViewActionsTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`
- `./scripts/platform-sail artisan test --compact`

## Outcome
- heavy-governance latest artifacts now agree on an authoritative `330s` threshold with `recalibrated` outcome after the honest rerun
- full suite result: `3760 passed`, `8 skipped`, `23535 assertions`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #242
2026-04-17 13:17:13 +00:00
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
75 changed files with 12239 additions and 798 deletions

View File

@ -0,0 +1,74 @@
name: Browser Lane
on:
workflow_dispatch:
schedule:
- cron: '43 4 * * 1-5'
jobs:
browser:
if: ${{ github.event_name != 'schedule' || vars.TENANTATLAS_ENABLE_BROWSER_SCHEDULE == '1' }}
runs-on: ubuntu-latest
env:
SAIL_TTY: 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
- name: Install platform dependencies
run: |
cd apps/platform
if [[ ! -f .env ]]; then
cp .env.example .env
fi
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Boot Sail
run: |
cd apps/platform
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate --force --no-interaction
- name: Resolve Browser context
id: context
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "workflow_id=browser-scheduled" >> "$GITHUB_OUTPUT"
echo "trigger_class=scheduled" >> "$GITHUB_OUTPUT"
else
echo "workflow_id=browser-manual" >> "$GITHUB_OUTPUT"
echo "trigger_class=manual" >> "$GITHUB_OUTPUT"
fi
- name: Run Browser lane
run: ./scripts/platform-test-lane browser --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Refresh Browser report
if: always()
run: ./scripts/platform-test-report browser --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Stage Browser artifacts
if: always()
run: ./scripts/platform-test-artifacts browser .gitea-artifacts/browser --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Upload Browser artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: browser-artifacts
path: .gitea-artifacts/browser
if-no-files-found: error
- name: Stop Sail
if: always()
run: |
cd apps/platform
./vendor/bin/sail stop

View File

@ -0,0 +1,74 @@
name: Heavy Governance Lane
on:
workflow_dispatch:
schedule:
- cron: '17 4 * * 1-5'
jobs:
heavy-governance:
if: ${{ github.event_name != 'schedule' || vars.TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE == '1' }}
runs-on: ubuntu-latest
env:
SAIL_TTY: 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
- name: Install platform dependencies
run: |
cd apps/platform
if [[ ! -f .env ]]; then
cp .env.example .env
fi
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Boot Sail
run: |
cd apps/platform
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate --force --no-interaction
- name: Resolve Heavy Governance context
id: context
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "workflow_id=heavy-governance-scheduled" >> "$GITHUB_OUTPUT"
echo "trigger_class=scheduled" >> "$GITHUB_OUTPUT"
else
echo "workflow_id=heavy-governance-manual" >> "$GITHUB_OUTPUT"
echo "trigger_class=manual" >> "$GITHUB_OUTPUT"
fi
- name: Run Heavy Governance lane
run: ./scripts/platform-test-lane heavy-governance --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Refresh Heavy Governance report
if: always()
run: ./scripts/platform-test-report heavy-governance --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Stage Heavy Governance artifacts
if: always()
run: ./scripts/platform-test-artifacts heavy-governance .gitea-artifacts/heavy-governance --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}
- name: Upload Heavy Governance artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: heavy-governance-artifacts
path: .gitea-artifacts/heavy-governance
if-no-files-found: error
- name: Stop Sail
if: always()
run: |
cd apps/platform
./vendor/bin/sail stop

View File

@ -0,0 +1,62 @@
name: Main Confidence
on:
push:
branches:
- dev
jobs:
confidence:
runs-on: ubuntu-latest
env:
SAIL_TTY: 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
- name: Install platform dependencies
run: |
cd apps/platform
if [[ ! -f .env ]]; then
cp .env.example .env
fi
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Boot Sail
run: |
cd apps/platform
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate --force --no-interaction
- name: Run Confidence lane
run: ./scripts/platform-test-lane confidence --workflow-id=main-confidence --trigger-class=mainline-push
- name: Refresh Confidence report
if: always()
run: ./scripts/platform-test-report confidence --workflow-id=main-confidence --trigger-class=mainline-push
- name: Stage Confidence artifacts
if: always()
run: ./scripts/platform-test-artifacts confidence .gitea-artifacts/main-confidence --workflow-id=main-confidence --trigger-class=mainline-push
- name: Upload Confidence artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: confidence-artifacts
path: .gitea-artifacts/main-confidence
if-no-files-found: error
- name: Stop Sail
if: always()
run: |
cd apps/platform
./vendor/bin/sail stop

View File

@ -0,0 +1,64 @@
name: PR Fast Feedback
on:
pull_request:
types:
- opened
- reopened
- synchronize
jobs:
fast-feedback:
runs-on: ubuntu-latest
env:
SAIL_TTY: 'false'
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
coverage: none
tools: composer:v2
- name: Install platform dependencies
run: |
cd apps/platform
if [[ ! -f .env ]]; then
cp .env.example .env
fi
composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Boot Sail
run: |
cd apps/platform
./vendor/bin/sail up -d
./vendor/bin/sail artisan key:generate --force --no-interaction
- name: Run Fast Feedback lane
run: ./scripts/platform-test-lane fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request
- name: Refresh Fast Feedback report
if: always()
run: ./scripts/platform-test-report fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request
- name: Stage Fast Feedback artifacts
if: always()
run: ./scripts/platform-test-artifacts fast-feedback .gitea-artifacts/pr-fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request
- name: Upload Fast Feedback artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: fast-feedback-artifacts
path: .gitea-artifacts/pr-fast-feedback
if-no-files-found: error
- name: Stop Sail
if: always()
run: |
cd apps/platform
./vendor/bin/sail stop

View File

@ -194,6 +194,10 @@ ## Active Technologies
- SQLite `:memory:` for the default test configuration, dedicated PostgreSQL config for the schema-level `Pgsql` suite, and local runner artifacts under `apps/platform/storage/logs/test-lanes` (206-test-suite-governance) - SQLite `:memory:` for the default test configuration, dedicated PostgreSQL config for the schema-level `Pgsql` suite, and local runner artifacts under `apps/platform/storage/logs/test-lanes` (206-test-suite-governance)
- PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail (207-shared-test-fixture-slimming) - PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail (207-shared-test-fixture-slimming)
- SQLite `:memory:` for the default test environment, isolated PostgreSQL coverage via the existing dedicated suite, and lane-measurement artifacts under the app-root contract path `storage/logs/test-lanes` (207-shared-test-fixture-slimming) - SQLite `:memory:` for the default test environment, isolated PostgreSQL coverage via the existing dedicated suite, and lane-measurement artifacts under the app-root contract path `storage/logs/test-lanes` (207-shared-test-fixture-slimming)
- SQLite `:memory:` for the default test environment, existing lane artifacts under the app-root contract path `storage/logs/test-lanes`, and no new product persistence (208-heavy-suite-segmentation)
- SQLite `:memory:` for the default test environment, mixed database strategy for some heavy-governance families as declared in `TestLaneManifest`, and existing lane artifacts under the app-root contract path `storage/logs/test-lanes` (209-heavy-governance-cost)
- PHP 8.4.15 for repo-truth test governance, Bash for repo-root wrappers, and GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/` + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams (210-ci-matrix-budget-enforcement)
- SQLite `:memory:` for default lane execution, filesystem artifacts under the app-root contract path `storage/logs/test-lanes`, checked-in workflow YAML under `.gitea/workflows/`, and no new product database persistence (210-ci-matrix-budget-enforcement)
- PHP 8.4.15 (feat/005-bulk-operations) - PHP 8.4.15 (feat/005-bulk-operations)
@ -228,8 +232,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 207-shared-test-fixture-slimming: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail - 210-ci-matrix-budget-enforcement: Added PHP 8.4.15 for repo-truth test governance, Bash for repo-root wrappers, and GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/` + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams
- 206-test-suite-governance: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Pest Browser plugin, Filament v5, Livewire v4, Laravel Sail - 209-heavy-governance-cost: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
- 198-monitoring-page-state: Added PHP 8.4.15 + Laravel 12, Filament v5, Livewire v4, Pest v4, Tailwind CSS v4, existing `CanonicalAdminTenantFilterState`, `CanonicalNavigationContext`, `OperateHubShell`, Filament `InteractsWithTable`, and page-local Livewire state on the affected Filament pages - 208-heavy-suite-segmentation: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

View File

@ -10,5 +10,6 @@ ## Important
- `plan.md` - `plan.md`
- `tasks.md` - `tasks.md`
- `checklists/requirements.md` - `checklists/requirements.md`
- Runtime-changing work MUST carry testing/lane/runtime impact through the active `spec.md`, `plan.md`, and `tasks.md`; lane upkeep belongs to the feature, not to a later cleanup pass.
The files `.specify/spec.md`, `.specify/plan.md`, `.specify/tasks.md` may exist as legacy references only. The files `.specify/spec.md`, `.specify/plan.md`, `.specify/tasks.md` may exist as legacy references only.

View File

@ -1,37 +1,32 @@
<!-- <!--
Sync Impact Report Sync Impact Report
- Version change: 2.2.0 -> 2.3.0 - Version change: 2.3.0 -> 2.4.0
- Modified principles: - Modified principles:
- UI-CONST-001: expanded to make TenantPilot's decision-first - Quality Gates: expanded to require narrowest-lane validation and
governance identity explicit runtime-drift notes for runtime changes
- UI-REVIEW-001: spec and PR review gates expanded for surface role, - Governance review expectations: expanded to make lane/runtime
human-in-the-loop justification, workflow-vs-storage IA, and impact a mandatory part of spec and PR review
attention-load reduction
- Immediate Retrofit Priorities: expanded with a classification-first
wave for existing surfaces
- Added sections: - Added sections:
- Decision-First Operating Model & Progressive Disclosure - Test Suite Governance Must Live In The Delivery Workflow
(DECIDE-001) (TEST-GOV-001)
- Removed sections: None - Removed sections: None
- Templates requiring updates: - Templates requiring updates:
- ✅ .specify/memory/constitution.md - ✅ .specify/memory/constitution.md
- ✅ .specify/templates/plan-template.md (Constitution Check updated for - ✅ .specify/templates/plan-template.md (test-governance planning and
decision-first surface roles, workflow-first IA, and calm-surface lane-impact checks added)
review) - ✅ .specify/templates/spec-template.md (mandatory testing/lane/runtime
- ✅ .specify/templates/spec-template.md (surface role classification, impact section added)
operator contract, and requirements updated for decision-first - ✅ .specify/templates/tasks-template.md (lane classification,
governance) fixture-cost, and runtime-drift task guidance added)
- ✅ .specify/templates/tasks-template.md (implementation task guidance - ✅ .specify/templates/checklist-template.md (runtime checklist note
updated for progressive disclosure, single-case context, and added)
attention-load reduction) - ✅ .specify/README.md (SpecKit workflow note added for lane/runtime
- ✅ docs/product/standards/README.md (Constitution index updated for ownership)
DECIDE-001) - ✅ README.md (developer routine updated for test-governance upkeep)
- Commands checked: - Commands checked:
- N/A `.specify/templates/commands/*.md` directory is not present in this repo - N/A `.specify/templates/commands/*.md` directory is not present in this repo
- Follow-up TODOs: - Follow-up TODOs: None
- Create a dedicated surface / IA classification spec to retrofit
existing surfaces against DECIDE-001.
--> -->
# TenantPilot Constitution # TenantPilot Constitution
@ -107,6 +102,13 @@ ### Tests Must Protect Business Truth (TEST-TRUTH-001)
- Large dedicated test surfaces for thin presentation indirection SHOULD be avoided. - Large dedicated test surfaces for thin presentation indirection SHOULD be avoided.
- If a pattern creates more test burden than product certainty, the pattern SHOULD be simplified. - If a pattern creates more test burden than product certainty, the pattern SHOULD be simplified.
### Test Suite Governance Must Live In The Delivery Workflow (TEST-GOV-001)
- Test-suite governance is a standing workflow rule, not an occasional cleanup project.
- Every runtime-changing spec MUST declare the affected validation lane(s), any fixture/helper cost risk, whether it introduces or expands heavy-governance or browser coverage, and whether budget/baseline follow-up is needed.
- Plans MUST choose the narrowest lane mix that proves the change and MUST call out new heavy families, expensive defaults, or CI/runtime drift before implementation starts.
- Tasks and reviews MUST confirm lane classification, keep default fixtures cheap, reject accidental heavy promotion, and record material runtime drift or recalibration work in the active spec or PR.
- Standalone follow-up specs for test governance are reserved for recurring pain or structural lane changes; ordinary recalibration belongs inside normal delivery work.
### Enterprise Complexity Is Allowed Only Where Risk Demands It (RISK-COMP-001) ### Enterprise Complexity Is Allowed Only Where Risk Demands It (RISK-COMP-001)
- Heavier architecture is explicitly legitimate for workspace or tenant isolation, RBAC and policy enforcement, auditability, immutable history and snapshot truth, queue/job execution legitimacy, provider credential safety, retention/compliance evidence, and operator-critical lifecycle correctness. - Heavier architecture is explicitly legitimate for workspace or tenant isolation, RBAC and policy enforcement, auditability, immutable history and snapshot truth, queue/job execution legitimacy, provider credential safety, retention/compliance evidence, and operator-critical lifecycle correctness.
- Badge systems, explanation builders, trust/confidence overlays, presentation taxonomies, generic provider frameworks without real provider variance, speculative export/report/review infrastructure, UI meta-governance frameworks, and derived helper entities promoted into persisted truth are high-risk overproduction zones and require extra restraint. - Badge systems, explanation builders, trust/confidence overlays, presentation taxonomies, generic provider frameworks without real provider variance, speculative export/report/review infrastructure, UI meta-governance frameworks, and derived helper entities promoted into persisted truth are high-risk overproduction zones and require extra restraint.
@ -1326,6 +1328,7 @@ ### Spec-First Workflow
## Quality Gates ## Quality Gates
- Changes MUST be programmatically tested (Pest) and run via targeted `php artisan test ...`. - Changes MUST be programmatically tested (Pest) and run via targeted `php artisan test ...`.
- Runtime changes MUST validate the narrowest relevant lane and document any material budget, baseline, or trend follow-up in the active spec or PR.
- Run `./vendor/bin/sail bin pint --dirty` before finalizing. - Run `./vendor/bin/sail bin pint --dirty` before finalizing.
## Governance ## Governance
@ -1334,9 +1337,11 @@ ### Scope, Compliance, and Review Expectations
- This constitution applies across the repo. Feature specs may add stricter constraints but not weaker ones. - This constitution applies across the repo. Feature specs may add stricter constraints but not weaker ones.
- Restore semantics changes require: spec update, checklist update (if applicable), and tests proving safety. - Restore semantics changes require: spec update, checklist update (if applicable), and tests proving safety.
- Specs and PRs that introduce new persisted truth, abstractions, states, DTO/presenter layers, or taxonomies MUST include the proportionality review required by BLOAT-001. - Specs and PRs that introduce new persisted truth, abstractions, states, DTO/presenter layers, or taxonomies MUST include the proportionality review required by BLOAT-001.
- Runtime-changing specs and PRs MUST include testing/lane/runtime impact covering affected lanes, fixture/helper cost changes, any heavy-family expansion, expected budget/baseline effect, and the minimal validation commands.
- Specs and PRs that change operator-facing surfaces MUST classify each - Specs and PRs that change operator-facing surfaces MUST classify each
affected surface under DECIDE-001 and justify any new Primary affected surface under DECIDE-001 and justify any new Primary
Decision Surface or workflow-first navigation change. Decision Surface or workflow-first navigation change.
- Reviews MUST reject runtime changes when lane classification is missing, expensive defaults are introduced silently, or material CI/runtime drift is left undocumented.
- Review and approval MUST favor simplification, replacement, and absorption over additive semantic layering. - Review and approval MUST favor simplification, replacement, and absorption over additive semantic layering.
- Future-release preparation alone is not sufficient justification for new persistence or frameworkization unless security, tenant isolation, auditability, compliance evidence, or queue correctness already require it. - Future-release preparation alone is not sufficient justification for new persistence or frameworkization unless security, tenant isolation, auditability, compliance evidence, or queue correctness already require it.
@ -1350,4 +1355,4 @@ ### Versioning Policy (SemVer)
- **MINOR**: new principle/section or materially expanded guidance. - **MINOR**: new principle/section or materially expanded guidance.
- **MAJOR**: removing/redefining principles in a backward-incompatible way. - **MAJOR**: removing/redefining principles in a backward-incompatible way.
**Version**: 2.3.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-12 **Version**: 2.4.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-17

View File

@ -5,6 +5,7 @@ # [CHECKLIST TYPE] Checklist: [FEATURE NAME]
**Feature**: [Link to spec.md or relevant documentation] **Feature**: [Link to spec.md or relevant documentation]
**Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements. **Note**: This checklist is generated by the `/speckit.checklist` command based on feature context and requirements.
If the checklist covers runtime behavior changes, include lane classification, fixture-cost review, heavy-family justification, minimal validation commands, and any budget/baseline follow-up checks.
<!-- <!--
============================================================================ ============================================================================

View File

@ -21,6 +21,7 @@ ## Technical Context
**Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION] **Primary Dependencies**: [e.g., FastAPI, UIKit, LLVM or NEEDS CLARIFICATION]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A] **Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION] **Testing**: [e.g., pytest, XCTest, cargo test or NEEDS CLARIFICATION]
**Validation Lanes**: [e.g., fast-feedback, confidence or NEEDS CLARIFICATION]
**Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION] **Target Platform**: [e.g., Linux server, iOS 15+, WASM or NEEDS CLARIFICATION]
**Project Type**: [single/web/mobile - determines source structure] **Project Type**: [single/web/mobile - determines source structure]
**Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION] **Performance Goals**: [domain-specific, e.g., 1000 req/s, 10k lines/sec, 60 fps or NEEDS CLARIFICATION]
@ -48,6 +49,7 @@ ## Constitution Check
- Ops-UX system runs: initiator-null runs emit no terminal DB notification; audit remains via Monitoring; tenant-wide alerting goes through Alerts (not OperationRun notifications) - Ops-UX system runs: initiator-null runs emit no terminal DB notification; audit remains via Monitoring; tenant-wide alerting goes through Alerts (not OperationRun notifications)
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter - Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens - Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Test governance (TEST-GOV-001): affected lanes, fixture/helper cost risks, heavy-family changes, and any budget/baseline follow-up are explicit; the narrowest proving lane is planned
- Proportionality (PROP-001): any new structure, layer, persisted truth, or semantic machinery is justified by current release truth, current operator workflow, and why a narrower solution is insufficient - Proportionality (PROP-001): any new structure, layer, persisted truth, or semantic machinery is justified by current release truth, current operator workflow, and why a narrower solution is insufficient
- No premature abstraction (ABSTR-001): no new factories, registries, resolvers, strategy systems, interfaces, type registries, or orchestration pipelines before at least 2 real concrete cases exist, unless security, tenant isolation, auditability, compliance evidence, or queue correctness require it now - No premature abstraction (ABSTR-001): no new factories, registries, resolvers, strategy systems, interfaces, type registries, or orchestration pipelines before at least 2 real concrete cases exist, unless security, tenant isolation, auditability, compliance evidence, or queue correctness require it now
- Persisted truth (PERSIST-001): new tables/entities/artifacts represent independent product truth or lifecycle; convenience projections and UI helpers stay derived - Persisted truth (PERSIST-001): new tables/entities/artifacts represent independent product truth or lifecycle; convenience projections and UI helpers stay derived
@ -87,6 +89,18 @@ ## Constitution Check
selection actions, navigation, and object actions; risky or rare selection actions, navigation, and object actions; risky or rare
actions are grouped and ordered by meaning/frequency/risk; any special actions are grouped and ordered by meaning/frequency/risk; any special
type or workflow-hub exception is explicit and justified type or workflow-hub exception is explicit and justified
## Test Governance Check
> **Fill for any runtime-changing feature. Docs-only or template-only work may state `N/A`.**
- **Affected validation lanes**: [fast-feedback / confidence / heavy-governance / browser / profiling / junit / N/A]
- **Narrowest proving command(s)**: [Exact commands reviewers should run before merge]
- **Fixture / helper cost risks**: [none / describe]
- **Heavy-family additions or promotions**: [none / describe]
- **Budget / baseline / trend follow-up**: [none / describe]
- **Why no dedicated follow-up spec is needed**: [Routine upkeep stays inside this feature unless recurring pain or structural lane changes justify a separate spec]
## Project Structure ## Project Structure
### Documentation (this feature) ### Documentation (this feature)

View File

@ -88,6 +88,18 @@ ## Proportionality Review *(mandatory when structural complexity is introduced)*
- **Alternative intentionally rejected**: [What simpler option was considered and why it was not sufficient] - **Alternative intentionally rejected**: [What simpler option was considered and why it was not sufficient]
- **Release truth**: [Current-release truth or future-release preparation] - **Release truth**: [Current-release truth or future-release preparation]
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
For docs-only changes, state `N/A` for each field.
- **Validation lane(s)**: [fast-feedback / confidence / heavy-governance / browser / profiling / junit / N/A]
- **Why these lanes are sufficient**: [Why the narrowest listed lane(s) prove the change]
- **New or expanded test families**: [none / describe]
- **Fixture / helper cost impact**: [none / describe new defaults, factories, seeds, helpers, browser setup, etc.]
- **Heavy coverage justification**: [none / explain any heavy-governance or browser addition]
- **Budget / baseline / trend impact**: [none / expected drift + follow-up]
- **Planned validation commands**: [Exact minimal commands reviewers should run]
## User Scenarios & Testing *(mandatory)* ## User Scenarios & Testing *(mandatory)*
<!-- <!--
@ -175,6 +187,13 @@ ## Requirements *(mandatory)*
If the feature introduces a new enum/status family, DTO/presenter/envelope, persisted entity/table, interface/contract/registry/resolver, If the feature introduces a new enum/status family, DTO/presenter/envelope, persisted entity/table, interface/contract/registry/resolver,
or taxonomy/classification system, the Proportionality Review section above is mandatory. or taxonomy/classification system, the Proportionality Review section above is mandatory.
**Constitution alignment (TEST-GOV-001):** If this feature changes runtime behavior or tests, the spec MUST describe:
- the affected validation lane(s) and why they are the narrowest sufficient proof,
- any new or expanded heavy-governance or browser coverage,
- any fixture, helper, factory, seed, or default setup cost added or avoided,
- any expected budget, baseline, or trend impact,
- and the exact minimal validation commands reviewers should run.
**Constitution alignment (OPS-UX):** If this feature creates/reuses an `OperationRun`, the spec MUST: **Constitution alignment (OPS-UX):** If this feature creates/reuses an `OperationRun`, the spec MUST:
- explicitly state compliance with the Ops-UX 3-surface feedback contract (toast intent-only, progress surfaces, terminal DB notification), - explicitly state compliance with the Ops-UX 3-surface feedback contract (toast intent-only, progress surfaces, terminal DB notification),
- state that `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`), - state that `OperationRun.status` / `OperationRun.outcome` transitions are service-owned (only via `OperationRunService`),

View File

@ -9,6 +9,12 @@ # Tasks: [FEATURE NAME]
**Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/ **Prerequisites**: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
**Tests**: For runtime behavior changes in this repo, tests are REQUIRED (Pest). Only docs-only changes may omit tests. **Tests**: For runtime behavior changes in this repo, tests are REQUIRED (Pest). Only docs-only changes may omit tests.
Runtime-changing features MUST also include tasks to:
- classify or confirm the affected validation lane(s),
- keep new helpers, factories, and seeds cheap by default or isolate expensive setup behind explicit opt-ins,
- justify any new heavy-governance or browser coverage,
- run the narrowest relevant lane before merge,
- and record budget, baseline, or trend follow-up when runtime cost shifts materially.
**Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a **Operations**: If this feature introduces long-running/remote/queued/scheduled work, include tasks to create/reuse and update a
canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub. canonical `OperationRun`, and ensure “View run” links route to the canonical Monitoring hub.
If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant). If security-relevant DB-only actions skip `OperationRun`, include tasks for `AuditLog` entries (before/after + actor + tenant).
@ -123,6 +129,7 @@ # Tasks: [FEATURE NAME]
- and adding tests around business consequences, permissions, lifecycle behavior, isolation, or audit responsibilities rather than thin indirection alone. - and adding tests around business consequences, permissions, lifecycle behavior, isolation, or audit responsibilities rather than thin indirection alone.
**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story.
Runtime behavior changes SHOULD include at least one explicit task for lane validation or runtime-impact review so upkeep stays inside the feature instead of becoming separate cleanup.
## Format: `[ID] [P?] [Story] Description` ## Format: `[ID] [P?] [Story] Description`

View File

@ -64,6 +64,26 @@ ### Canonical Lane Commands
- `cd apps/platform && ./vendor/bin/sail composer run test:junit` - `cd apps/platform && ./vendor/bin/sail composer run test:junit`
- The root wrapper is the safer default for long lanes because it pins Composer to `--timeout=0`. - The root wrapper is the safer default for long lanes because it pins Composer to `--timeout=0`.
### Workflow Expectation
- Every runtime-changing spec, plan, and task set MUST record the target validation lane(s), fixture-cost risks, any heavy-governance or browser expansion, and any budget/baseline follow-up.
- Routine lane recalibration belongs inside the affecting feature spec or PR; open a dedicated follow-up spec only when recurring pain or structural lane changes justify it.
### CI Trigger Matrix
- Pull requests (`opened`, `reopened`, `synchronize`) run only `./scripts/platform-test-lane fast-feedback` through `.gitea/workflows/test-pr-fast-feedback.yml` and block on test, wrapper, artifact, and mature Fast Feedback budget failures.
- Pushes to `dev` run only `./scripts/platform-test-lane confidence` through `.gitea/workflows/test-main-confidence.yml`; test and artifact failures block, while budget drift remains warning-first.
- Heavy Governance runs live in `.gitea/workflows/test-heavy-governance.yml` and stay isolated to manual plus scheduled triggers. The schedule remains gated behind `TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE=1` until the first successful manual validation.
- Browser runs live in `.gitea/workflows/test-browser.yml` and stay isolated to manual plus scheduled triggers. The schedule remains gated behind `TENANTATLAS_ENABLE_BROWSER_SCHEDULE=1` until the first successful manual validation.
- Fast Feedback uses a documented CI variance allowance of `15s` before budget overrun becomes blocking. Confidence uses `30s`, Browser uses `20s`, and Heavy Governance keeps warning-first or trend-only handling while its CI baseline stabilizes.
### CI Artifact Bundles
- Lane-local artifacts are still generated in `apps/platform/storage/logs/test-lanes` as `*-latest.*` files.
- CI workflows stage deterministic upload bundles through `./scripts/platform-test-artifacts` into `.gitea-artifacts/<workflow-profile>` before upload.
- Every governed CI lane publishes `summary.md`, `budget.json`, `report.json`, and `junit.xml`. `profiling` may additionally publish `profile.txt`.
- Artifact publication failures are first-class blocking failures for pull request and `dev` workflows.
### Recorded Baselines ### Recorded Baselines
| Scope | Wall clock | Budget | Notes | | Scope | Wall clock | Budget | Notes |

View File

@ -80,6 +80,21 @@
"@php artisan config:clear --ansi", "@php artisan config:clear --ansi",
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::runLane('junit'));\"" "@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::runLane('junit'));\""
], ],
"test:report": [
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('fast-feedback', 'shared-test-fixture-slimming'));\""
],
"test:report:confidence": [
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('confidence', 'shared-test-fixture-slimming'));\""
],
"test:report:browser": [
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('browser', ''));\""
],
"test:report:heavy": [
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('heavy-governance', ''));\""
],
"test:report:profile": [
"@php -r \"require 'vendor/autoload.php'; exit(\\Tests\\Support\\TestLaneManifest::renderLatestReport('profiling', ''));\""
],
"test:pgsql": [ "test:pgsql": [
"Composer\\Config::disableProcessTimeout", "Composer\\Config::disableProcessTimeout",
"@php vendor/bin/pest -c phpunit.pgsql.xml" "@php vendor/bin/pest -c phpunit.pgsql.xml"

View File

@ -4,9 +4,6 @@
use App\Filament\Pages\BaselineCompareLanding; use App\Filament\Pages\BaselineCompareLanding;
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile; use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Facades\Filament; use Filament\Facades\Filament;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@ -16,22 +13,7 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedActiveBaselineForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->complete()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -50,22 +32,7 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ seedActiveBaselineForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->complete()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
$tenant->makeCurrent(); $tenant->makeCurrent();
Filament::setTenant($tenant, true); Filament::setTenant($tenant, true);
@ -83,22 +50,7 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedActiveBaselineForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->complete()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
DB::table('baseline_profiles') DB::table('baseline_profiles')
->where('id', (int) $profile->getKey()) ->where('id', (int) $profile->getKey())

View File

@ -7,6 +7,7 @@
use App\Jobs\CaptureBaselineSnapshotJob; use App\Jobs\CaptureBaselineSnapshotJob;
use App\Models\BaselineProfile; use App\Models\BaselineProfile;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Baselines\BaselineCaptureMode; use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Workspaces\WorkspaceContext; use App\Support\Workspaces\WorkspaceContext;
use Filament\Actions\Action; use Filament\Actions\Action;
@ -30,12 +31,21 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
return $instance->getCachedHeaderActions(); return $instance->getCachedHeaderActions();
} }
function seedCaptureProfileForTenant(
Tenant $tenant,
BaselineCaptureMode $captureMode = BaselineCaptureMode::FullContent,
array $attributes = [],
): BaselineProfile {
return BaselineProfile::factory()->active()->create(array_merge([
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => $captureMode->value,
], $attributes));
}
it('redirects unauthenticated users (302) when accessing the capture start surface', function (): void { it('redirects unauthenticated users (302) when accessing the capture start surface', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -47,9 +57,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
[$otherUser, $otherTenant] = createUserWithTenant(role: 'owner'); [$otherUser, $otherTenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $otherTenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $otherTenant->workspace_id);
@ -63,9 +71,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'readonly'); [$user, $tenant] = createUserWithTenant(role: 'readonly');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -86,10 +92,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => BaselineCaptureMode::FullContent->value,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -129,10 +132,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => BaselineCaptureMode::FullContent->value,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -152,8 +152,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
it('shows readiness copy without exposing raw canonical scope json on the capture start surface', function (): void { it('shows readiness copy without exposing raw canonical scope json on the capture start surface', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic, [
'workspace_id' => (int) $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], 'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]); ]);
@ -172,9 +171,7 @@ function baselineProfileCaptureHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ $profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
DB::table('baseline_profiles') DB::table('baseline_profiles')
->where('id', (int) $profile->getKey()) ->where('id', (int) $profile->getKey())

View File

@ -8,6 +8,7 @@
use App\Models\BaselineProfile; use App\Models\BaselineProfile;
use App\Models\BaselineSnapshot; use App\Models\BaselineSnapshot;
use App\Models\BaselineTenantAssignment; use App\Models\BaselineTenantAssignment;
use App\Models\Tenant;
use App\Models\OperationRun; use App\Models\OperationRun;
use App\Support\Baselines\BaselineCaptureMode; use App\Support\Baselines\BaselineCaptureMode;
use App\Support\Baselines\Compare\CompareStrategyRegistry; use App\Support\Baselines\Compare\CompareStrategyRegistry;
@ -34,29 +35,27 @@ function baselineProfileHeaderActions(Testable $component): array
return $instance->getCachedHeaderActions(); return $instance->getCachedHeaderActions();
} }
/**
* @return array{0: BaselineProfile, 1: BaselineSnapshot}
*/
function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureMode $captureMode = BaselineCaptureMode::FullContent): array
{
[$profile, $snapshot] = seedActiveBaselineForTenant($tenant);
$profile->forceFill([
'capture_mode' => $captureMode->value,
])->save();
return [$profile->fresh(), $snapshot];
}
it('does not start baseline compare for workspace members missing tenant.sync', function (): void { it('does not start baseline compare for workspace members missing tenant.sync', function (): void {
Queue::fake(); Queue::fake();
config()->set('tenantpilot.baselines.full_content_capture.enabled', true); config()->set('tenantpilot.baselines.full_content_capture.enabled', true);
[$user, $tenant] = createUserWithTenant(role: 'readonly'); [$user, $tenant] = createUserWithTenant(role: 'readonly');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => BaselineCaptureMode::FullContent->value,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -77,23 +76,7 @@ function baselineProfileHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => BaselineCaptureMode::FullContent->value,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -123,23 +106,7 @@ function baselineProfileHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant);
'workspace_id' => (int) $tenant->workspace_id,
'capture_mode' => BaselineCaptureMode::FullContent->value,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -167,7 +134,9 @@ function baselineProfileHeaderActions(Testable $component): array
app(FakeCompareStrategy::class), app(FakeCompareStrategy::class),
])); ]));
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
$profile->forceFill([
'workspace_id' => (int) $tenant->workspace_id, 'workspace_id' => (int) $tenant->workspace_id,
'scope_jsonb' => [ 'scope_jsonb' => [
'version' => 2, 'version' => 2,
@ -186,20 +155,7 @@ function baselineProfileHeaderActions(Testable $component): array
], ],
], ],
], ],
]); ])->save();
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -218,22 +174,7 @@ function baselineProfileHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -268,22 +209,7 @@ function baselineProfileHeaderActions(Testable $component): array
it('keeps compare-assigned-tenants visible but disabled for readonly workspace members after the navigation move', function (): void { it('keeps compare-assigned-tenants visible but disabled for readonly workspace members after the navigation move', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'readonly'); [$user, $tenant] = createUserWithTenant(role: 'readonly');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -299,23 +225,11 @@ function baselineProfileHeaderActions(Testable $component): array
it('shows readiness copy without exposing raw canonical scope json on the compare start surface', function (): void { it('shows readiness copy without exposing raw canonical scope json on the compare start surface', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
$profile->forceFill([
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], 'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]); ])->save();
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -332,22 +246,7 @@ function baselineProfileHeaderActions(Testable $component): array
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = BaselineProfile::factory()->active()->create([ [$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic);
'workspace_id' => (int) $tenant->workspace_id,
]);
$snapshot = BaselineSnapshot::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'baseline_profile_id' => (int) $profile->getKey(),
]);
$profile->update(['active_snapshot_id' => (int) $snapshot->getKey()]);
BaselineTenantAssignment::factory()->create([
'workspace_id' => (int) $tenant->workspace_id,
'tenant_id' => (int) $tenant->getKey(),
'baseline_profile_id' => (int) $profile->getKey(),
]);
DB::table('baseline_profiles') DB::table('baseline_profiles')
->where('id', (int) $profile->getKey()) ->where('id', (int) $profile->getKey())

View File

@ -12,29 +12,41 @@
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
function findingBulkAuditResourceIds(int $tenantId, string $action): array
{
return AuditLog::query()
->where('tenant_id', $tenantId)
->where('action', $action)
->pluck('resource_id')
->map(static fn (string $resourceId): int => (int) $resourceId)
->sort()
->values()
->all();
}
it('supports bulk workflow actions and audits each record', function (): void { it('supports bulk workflow actions and audits each record', function (): void {
[$manager, $tenant] = createUserWithTenant(role: 'manager'); [$manager, $tenant] = createUserWithTenant(role: 'manager');
$this->actingAs($manager); $this->actingAs($manager);
Filament::setTenant($tenant, true); Filament::setTenant($tenant, true);
$findings = Finding::factory() $findings = Finding::factory()
->count(101) ->count(6)
->for($tenant) ->for($tenant)
->create([ ->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
'triaged_at' => null, 'triaged_at' => null,
]); ]);
Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class);
$component
->callTableBulkAction('triage_selected', $findings) ->callTableBulkAction('triage_selected', $findings)
->assertHasNoTableBulkActionErrors(); ->assertHasNoTableBulkActionErrors();
$findings->each(fn (Finding $finding) => expect($finding->refresh()->status)->toBe(Finding::STATUS_TRIAGED)); $findings->each(fn (Finding $finding) => expect($finding->refresh()->status)->toBe(Finding::STATUS_TRIAGED));
expect(AuditLog::query() expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.triaged'))
->where('tenant_id', (int) $tenant->getKey()) ->toEqual($findings->pluck('id')->sort()->values()->all());
->where('action', 'finding.triaged')
->count())->toBe(101);
$assignee = User::factory()->create(); $assignee = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator'); createUserWithTenant(tenant: $tenant, user: $assignee, role: 'operator');
@ -48,7 +60,7 @@
'owner_user_id' => null, 'owner_user_id' => null,
]); ]);
Livewire::test(ListFindings::class) $component
->callTableBulkAction('assign_selected', $assignFindings, data: [ ->callTableBulkAction('assign_selected', $assignFindings, data: [
'assignee_user_id' => (int) $assignee->getKey(), 'assignee_user_id' => (int) $assignee->getKey(),
'owner_user_id' => (int) $manager->getKey(), 'owner_user_id' => (int) $manager->getKey(),
@ -62,10 +74,8 @@
->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey()); ->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey());
}); });
expect(AuditLog::query() expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.assigned'))
->where('tenant_id', (int) $tenant->getKey()) ->toEqual($assignFindings->pluck('id')->sort()->values()->all());
->where('action', 'finding.assigned')
->count())->toBe(3);
$resolveFindings = Finding::factory() $resolveFindings = Finding::factory()
->count(2) ->count(2)
@ -76,7 +86,7 @@
'resolved_reason' => null, 'resolved_reason' => null,
]); ]);
Livewire::test(ListFindings::class) $component
->callTableBulkAction('resolve_selected', $resolveFindings, data: [ ->callTableBulkAction('resolve_selected', $resolveFindings, data: [
'resolved_reason' => 'fixed', 'resolved_reason' => 'fixed',
]) ])
@ -90,10 +100,8 @@
->and($finding->resolved_at)->not->toBeNull(); ->and($finding->resolved_at)->not->toBeNull();
}); });
expect(AuditLog::query() expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.resolved'))
->where('tenant_id', (int) $tenant->getKey()) ->toEqual($resolveFindings->pluck('id')->sort()->values()->all());
->where('action', 'finding.resolved')
->count())->toBe(2);
$closeFindings = Finding::factory() $closeFindings = Finding::factory()
->count(2) ->count(2)
@ -104,7 +112,7 @@
'closed_reason' => null, 'closed_reason' => null,
]); ]);
Livewire::test(ListFindings::class) $component
->callTableBulkAction('close_selected', $closeFindings, data: [ ->callTableBulkAction('close_selected', $closeFindings, data: [
'closed_reason' => 'not applicable', 'closed_reason' => 'not applicable',
]) ])
@ -119,9 +127,6 @@
->and($finding->closed_by_user_id)->not->toBeNull(); ->and($finding->closed_by_user_id)->not->toBeNull();
}); });
expect(AuditLog::query() expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.closed'))
->where('tenant_id', (int) $tenant->getKey()) ->toEqual($closeFindings->pluck('id')->sort()->values()->all());
->where('action', 'finding.closed')
->count())->toBe(2);
}); });

View File

@ -19,6 +19,38 @@
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
/**
* @return array{0: User, 1: User, 2: \App\Models\Tenant, 3: Finding, 4: FindingExceptionService, 5: FindingException}
*/
function seedApprovedFindingExceptionWindow(): array
{
[$requester, $tenant] = createUserWithTenant(role: 'owner');
$approver = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$finding = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RISK_ACCEPTED,
]);
/** @var FindingExceptionService $service */
$service = app(FindingExceptionService::class);
$requested = $service->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Initial acceptance window',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
]);
$active = $service->approve($requested, $approver, [
'effective_from' => now()->subDay()->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
'approval_reason' => 'Initial approval',
]);
return [$requester, $approver, $tenant, $finding, $service, $active];
}
it('requests and approves a renewal while preserving prior decision history and evidence references', function (): void { it('requests and approves a renewal while preserving prior decision history and evidence references', function (): void {
[$requester, $tenant] = createUserWithTenant(role: 'owner'); [$requester, $tenant] = createUserWithTenant(role: 'owner');
$approver = User::factory()->create(); $approver = User::factory()->create();
@ -156,29 +188,7 @@
}); });
it('rejects a pending renewal without erasing the prior approved governance window', function (): void { it('rejects a pending renewal without erasing the prior approved governance window', function (): void {
[$requester, $tenant] = createUserWithTenant(role: 'owner'); [$requester, $approver, $tenant, $finding, $service, $active] = seedApprovedFindingExceptionWindow();
$approver = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$finding = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_RISK_ACCEPTED,
]);
/** @var FindingExceptionService $service */
$service = app(FindingExceptionService::class);
$requested = $service->request($finding, $tenant, $requester, [
'owner_user_id' => (int) $requester->getKey(),
'request_reason' => 'Initial acceptance window',
'review_due_at' => now()->addDays(5)->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
]);
$active = $service->approve($requested, $approver, [
'effective_from' => now()->subDay()->toDateTimeString(),
'expires_at' => now()->addDays(20)->toDateTimeString(),
'approval_reason' => 'Initial approval',
]);
$service->renew($active, $requester, [ $service->renew($active, $requester, [
'owner_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(),

View File

@ -23,7 +23,9 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class);
$component
->callTableAction('triage', $finding) ->callTableAction('triage', $finding)
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
@ -31,7 +33,7 @@
expect($finding->status)->toBe(Finding::STATUS_TRIAGED) expect($finding->status)->toBe(Finding::STATUS_TRIAGED)
->and($finding->triaged_at)->not->toBeNull(); ->and($finding->triaged_at)->not->toBeNull();
Livewire::test(ListFindings::class) $component
->callTableAction('start_progress', $finding) ->callTableAction('start_progress', $finding)
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
@ -39,7 +41,7 @@
expect($finding->status)->toBe(Finding::STATUS_IN_PROGRESS) expect($finding->status)->toBe(Finding::STATUS_IN_PROGRESS)
->and($finding->in_progress_at)->not->toBeNull(); ->and($finding->in_progress_at)->not->toBeNull();
Livewire::test(ListFindings::class) $component
->callTableAction('resolve', $finding, [ ->callTableAction('resolve', $finding, [
'resolved_reason' => 'patched', 'resolved_reason' => 'patched',
]) ])
@ -50,7 +52,7 @@
->and($finding->resolved_reason)->toBe('patched') ->and($finding->resolved_reason)->toBe('patched')
->and($finding->resolved_at)->not->toBeNull(); ->and($finding->resolved_at)->not->toBeNull();
Livewire::test(ListFindings::class) $component
->filterTable('open', false) ->filterTable('open', false)
->callTableAction('reopen', $finding, [ ->callTableAction('reopen', $finding, [
'reopen_reason' => 'The issue recurred in a later scan.', 'reopen_reason' => 'The issue recurred in a later scan.',
@ -76,13 +78,15 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class);
$component
->callTableAction('close', $closeFinding, [ ->callTableAction('close', $closeFinding, [
'closed_reason' => 'duplicate ticket', 'closed_reason' => 'duplicate ticket',
]) ])
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
Livewire::test(ListFindings::class) $component
->callTableAction('request_exception', $exceptionFinding, [ ->callTableAction('request_exception', $exceptionFinding, [
'owner_user_id' => (int) $user->getKey(), 'owner_user_id' => (int) $user->getKey(),
'request_reason' => 'accepted by security', 'request_reason' => 'accepted by security',
@ -117,7 +121,9 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class);
$component
->callTableAction('assign', $finding, [ ->callTableAction('assign', $finding, [
'assignee_user_id' => (int) $assignee->getKey(), 'assignee_user_id' => (int) $assignee->getKey(),
'owner_user_id' => (int) $manager->getKey(), 'owner_user_id' => (int) $manager->getKey(),
@ -128,7 +134,7 @@
expect((int) $finding->assignee_user_id)->toBe((int) $assignee->getKey()) expect((int) $finding->assignee_user_id)->toBe((int) $assignee->getKey())
->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey()); ->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey());
Livewire::test(ListFindings::class) $component
->callTableAction('assign', $finding, [ ->callTableAction('assign', $finding, [
'assignee_user_id' => (int) $outsider->getKey(), 'assignee_user_id' => (int) $outsider->getKey(),
'owner_user_id' => (int) $manager->getKey(), 'owner_user_id' => (int) $manager->getKey(),

View File

@ -54,7 +54,9 @@
$finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]); $finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]);
Livewire::test(ViewFinding::class, ['record' => $finding->getKey()]) $component = Livewire::test(ViewFinding::class, ['record' => $finding->getKey()]);
$component
->callAction('triage') ->callAction('triage')
->assertHasNoActionErrors() ->assertHasNoActionErrors()
->callAction('assign', [ ->callAction('assign', [

View File

@ -11,6 +11,16 @@
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
function actingAsFindingsManagerForFilters(): array
{
[$user, $tenant] = createUserWithTenant(role: 'manager');
test()->actingAs($user);
Filament::setTenant($tenant, true);
return [$user, $tenant];
}
function findingFilterIndicatorLabels($component): array function findingFilterIndicatorLabels($component): array
{ {
return collect($component->instance()->getTable()->getFilterIndicators()) return collect($component->instance()->getTable()->getFilterIndicators())
@ -19,9 +29,7 @@ function findingFilterIndicatorLabels($component): array
} }
it('filters findings by overdue quick filter using open statuses only', function (): void { it('filters findings by overdue quick filter using open statuses only', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$overdueOpen = Finding::factory()->for($tenant)->create([ $overdueOpen = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
@ -45,14 +53,11 @@ function findingFilterIndicatorLabels($component): array
}); });
it('filters findings by workflow family and governance validity', function (): void { it('filters findings by workflow family and governance validity', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
[$requester] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$requester] = createUserWithTenant(tenant: $tenant, role: 'owner');
$approver = User::factory()->create(); $approver = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager'); createUserWithTenant(tenant: $tenant, user: $approver, role: 'owner', workspaceRole: 'manager');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$active = Finding::factory()->for($tenant)->create([ $active = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
@ -105,9 +110,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('filters findings by high severity quick filter', function (): void { it('filters findings by high severity quick filter', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$critical = Finding::factory()->for($tenant)->create([ $critical = Finding::factory()->for($tenant)->create([
'severity' => Finding::SEVERITY_CRITICAL, 'severity' => Finding::SEVERITY_CRITICAL,
@ -131,9 +134,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('filters findings by my assigned quick filter', function (): void { it('filters findings by my assigned quick filter', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [$user, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$otherUser = User::factory()->create(); $otherUser = User::factory()->create();
createUserWithTenant(tenant: $tenant, user: $otherUser, role: 'operator'); createUserWithTenant(tenant: $tenant, user: $otherUser, role: 'operator');
@ -160,9 +161,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('persists findings search, sort, and filter state while keeping the resource pagination profile', function (): void { it('persists findings search, sort, and filter state while keeping the resource pagination profile', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$component = Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class)
->searchTable('drift') ->searchTable('drift')
@ -182,9 +181,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('composes status and created date filters with active indicators', function (): void { it('composes status and created date filters with active indicators', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$matching = Finding::factory()->for($tenant)->create([ $matching = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
@ -214,9 +211,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('clears findings filters back to the default open view', function (): void { it('clears findings filters back to the default open view', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$openFinding = Finding::factory()->for($tenant)->create([ $openFinding = Finding::factory()->for($tenant)->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
@ -250,9 +245,7 @@ function findingFilterIndicatorLabels($component): array
}); });
it('prefilters dashboard open-drift drill-throughs to the named findings subset', function (): void { it('prefilters dashboard open-drift drill-throughs to the named findings subset', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'manager'); [, $tenant] = actingAsFindingsManagerForFilters();
$this->actingAs($user);
Filament::setTenant($tenant, true);
$openDrift = Finding::factory()->for($tenant)->create([ $openDrift = Finding::factory()->for($tenant)->create([
'finding_type' => Finding::FINDING_TYPE_DRIFT, 'finding_type' => Finding::FINDING_TYPE_DRIFT,

View File

@ -126,9 +126,19 @@
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue; use Illuminate\Support\Facades\Queue;
use Livewire\Livewire; use Livewire\Livewire;
use Tests\Support\TestLaneManifest;
uses(RefreshDatabase::class); uses(RefreshDatabase::class);
it('keeps the retained action-surface contract family anchored in heavy-governance inventory', function (): void {
$inventoryRecord = collect(TestLaneManifest::heavyGovernanceHotspotInventory())
->firstWhere('familyId', 'action-surface-contract');
expect($inventoryRecord)->not->toBeNull()
->and($inventoryRecord['classificationId'])->toBe('surface-guard')
->and($inventoryRecord['status'])->toBe('retained');
});
function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser function actionSurfaceSystemPanelContext(array $capabilities): PlatformUser
{ {
Filament::setCurrentPanel('system'); Filament::setCurrentPanel('system');

View File

@ -4,20 +4,37 @@
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps browser tests isolated behind their dedicated lane', function (): void { it('keeps browser tests isolated behind their dedicated lane and class', function (): void {
$lane = TestLaneManifest::lane('browser'); $lane = TestLaneManifest::lane('browser');
$files = new Collection(TestLaneManifest::discoverFiles('browser')); $files = new Collection(TestLaneManifest::discoverFiles('browser'));
$validation = TestLaneManifest::validateLanePlacement(
laneId: 'browser',
filePath: 'tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php',
);
expect($lane['includedFamilies'])->toContain('browser') expect($lane['includedFamilies'])->toContain('browser')
->and($lane['defaultEntryPoint'])->toBeFalse() ->and($lane['defaultEntryPoint'])->toBeFalse()
->and($files)->not->toBeEmpty() ->and($files)->not->toBeEmpty()
->and($files->every(static fn (string $path): bool => str_starts_with($path, 'tests/Browser/')))->toBeTrue(); ->and($files->every(static fn (string $path): bool => str_starts_with($path, 'tests/Browser/')))->toBeTrue()
->and($validation['valid'])->toBeTrue()
->and($validation['resolvedClassificationId'])->toBe('browser')
->and($validation['familyId'])->toBe('browser-smoke');
}); });
it('keeps the browser lane routed to the browser suite and out of the default loops', function (): void { it('rejects browser placement in non-browser lanes and keeps the default loops clean', function (): void {
expect(TestLaneManifest::buildCommand('browser'))->toContain('--group=browser') $misplaced = TestLaneManifest::validateLanePlacement(
->and(TestLaneManifest::buildCommand('browser'))->toContain('--testsuite=Browser'); laneId: 'confidence',
filePath: 'tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php',
);
$configurationPath = TestLaneManifest::laneConfigurationPath('browser');
$configurationContents = (string) file_get_contents(TestLaneManifest::absolutePath($configurationPath));
expect(TestLaneManifest::buildCommand('browser'))->toContain('--configuration='.$configurationPath)
->and($configurationContents)->toContain('tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php')
->and($misplaced['valid'])->toBeFalse()
->and($misplaced['allowance'])->toBe('forbidden');
foreach (TestLaneManifest::discoverFiles('fast-feedback') as $path) { foreach (TestLaneManifest::discoverFiles('fast-feedback') as $path) {
expect($path)->not->toStartWith('tests/Browser/'); expect($path)->not->toStartWith('tests/Browser/');
@ -27,3 +44,13 @@
expect($path)->not->toStartWith('tests/Browser/'); expect($path)->not->toStartWith('tests/Browser/');
} }
}); });
it('keeps browser manual and scheduled budget profiles separate from the default contributor loops', function (): void {
$manualProfile = TestLaneBudget::enforcementProfile('browser', 'manual');
$scheduledProfile = TestLaneBudget::enforcementProfile('browser', 'scheduled');
expect($manualProfile['enforcementMode'])->toBe('soft-warn')
->and($scheduledProfile['enforcementMode'])->toBe('trend-only')
->and($manualProfile['effectiveThresholdSeconds'])->toBe(170)
->and($scheduledProfile['effectiveThresholdSeconds'])->toBe(170);
});

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
it('wires the dev push workflow to the confidence lane and publishes the lane-owned JUnit artifact bundle', function (): void {
$workflowProfile = TestLaneManifest::workflowProfile('main-confidence');
$workflowContents = (string) file_get_contents(repo_path($workflowProfile['filePath']));
expect(file_exists(repo_path($workflowProfile['filePath'])))->toBeTrue()
->and($workflowProfile['triggerClass'])->toBe('mainline-push')
->and($workflowProfile['branchFilters'])->toBe(['dev'])
->and($workflowContents)->toContain('push:')
->and($workflowContents)->toContain('- dev')
->and($workflowContents)->toContain('./scripts/platform-test-lane confidence --workflow-id=main-confidence --trigger-class=mainline-push')
->and($workflowContents)->toContain('./scripts/platform-test-report confidence --workflow-id=main-confidence --trigger-class=mainline-push')
->and($workflowContents)->toContain('./scripts/platform-test-artifacts confidence .gitea-artifacts/main-confidence --workflow-id=main-confidence --trigger-class=mainline-push')
->and($workflowContents)->toContain('name: confidence-artifacts')
->and($workflowContents)->not->toContain('test:junit', './scripts/platform-test-lane fast-feedback', './scripts/platform-test-lane heavy-governance');
});

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
it('wires the pull-request workflow only to the fast-feedback lane with the checked-in wrappers and artifact staging helper', function (): void {
$workflowProfile = TestLaneManifest::workflowProfile('pr-fast-feedback');
$workflowContents = (string) file_get_contents(repo_path($workflowProfile['filePath']));
expect(file_exists(repo_path($workflowProfile['filePath'])))->toBeTrue()
->and($workflowProfile['triggerClass'])->toBe('pull-request')
->and($workflowProfile['laneBindings'])->toBe(['fast-feedback'])
->and($workflowContents)->toContain('pull_request:')
->and($workflowContents)->toContain('opened', 'reopened', 'synchronize')
->and($workflowContents)->toContain('./scripts/platform-test-lane fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request')
->and($workflowContents)->toContain('./scripts/platform-test-report fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request')
->and($workflowContents)->toContain('./scripts/platform-test-artifacts fast-feedback .gitea-artifacts/pr-fast-feedback --workflow-id=pr-fast-feedback --trigger-class=pull-request')
->and($workflowContents)->toContain('name: fast-feedback-artifacts')
->and($workflowContents)->not->toContain('confidence --workflow-id=pr-fast-feedback', 'heavy-governance', 'browser --workflow-id=pr-fast-feedback');
});

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
it('keeps heavy-governance manual and scheduled execution inside the dedicated workflow file with schedule gating', function (): void {
$manualProfile = TestLaneManifest::workflowProfile('heavy-governance-manual');
$scheduledProfile = TestLaneManifest::workflowProfile('heavy-governance-scheduled');
$workflowContents = (string) file_get_contents(repo_path($manualProfile['filePath']));
expect(file_exists(repo_path($manualProfile['filePath'])))->toBeTrue()
->and($manualProfile['filePath'])->toBe($scheduledProfile['filePath'])
->and($manualProfile['laneBindings'])->toBe(['heavy-governance'])
->and($scheduledProfile['scheduleCron'])->toBe('17 4 * * 1-5')
->and($workflowContents)->toContain('workflow_dispatch:')
->and($workflowContents)->toContain('schedule:')
->and($workflowContents)->toContain('17 4 * * 1-5')
->and($workflowContents)->toContain("vars.TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE == '1'")
->and($workflowContents)->toContain('workflow_id=heavy-governance-manual')
->and($workflowContents)->toContain('workflow_id=heavy-governance-scheduled')
->and($workflowContents)->toContain('./scripts/platform-test-lane heavy-governance --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}')
->and($workflowContents)->toContain('./scripts/platform-test-artifacts heavy-governance .gitea-artifacts/heavy-governance --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}')
->and($workflowContents)->not->toContain('pull_request:', './scripts/platform-test-lane browser');
});
it('keeps browser manual and scheduled execution isolated from pull-request and confidence validation', function (): void {
$manualProfile = TestLaneManifest::workflowProfile('browser-manual');
$scheduledProfile = TestLaneManifest::workflowProfile('browser-scheduled');
$workflowContents = (string) file_get_contents(repo_path($manualProfile['filePath']));
expect(file_exists(repo_path($manualProfile['filePath'])))->toBeTrue()
->and($manualProfile['filePath'])->toBe($scheduledProfile['filePath'])
->and($manualProfile['laneBindings'])->toBe(['browser'])
->and($scheduledProfile['scheduleCron'])->toBe('43 4 * * 1-5')
->and($workflowContents)->toContain('workflow_dispatch:')
->and($workflowContents)->toContain('schedule:')
->and($workflowContents)->toContain('43 4 * * 1-5')
->and($workflowContents)->toContain("vars.TENANTATLAS_ENABLE_BROWSER_SCHEDULE == '1'")
->and($workflowContents)->toContain('workflow_id=browser-manual')
->and($workflowContents)->toContain('workflow_id=browser-scheduled')
->and($workflowContents)->toContain('./scripts/platform-test-lane browser --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}')
->and($workflowContents)->toContain('./scripts/platform-test-artifacts browser .gitea-artifacts/browser --workflow-id=${{ steps.context.outputs.workflow_id }} --trigger-class=${{ steps.context.outputs.trigger_class }}')
->and($workflowContents)->not->toContain('pull_request:', './scripts/platform-test-lane confidence');
});

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
use Tests\Support\TestLaneBudget;
use Tests\Support\TestLaneReport;
it('classifies wrapper or manifest drift before lower-level lane failures', function (): void {
$primaryFailureClassId = TestLaneReport::classifyPrimaryFailure(
exitCode: 1,
artifactPublicationStatus: ['complete' => true],
budgetOutcome: ['budgetStatus' => 'over-budget'],
entryPointResolved: false,
workflowLaneMatched: false,
);
expect($primaryFailureClassId)->toBe('wrapper-failure');
});
it('keeps confidence budget overruns visible without converting them into blocking failures', function (): void {
$budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 481.0);
$summary = TestLaneReport::buildCiSummary(
report: [
'laneId' => 'confidence',
'budgetStatus' => 'within-budget',
'ciContext' => ['workflowId' => 'main-confidence'],
],
exitCode: 0,
budgetOutcome: $budgetOutcome,
artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []],
);
expect($budgetOutcome['budgetStatus'])->toBe('over-budget')
->and($summary['primaryFailureClassId'])->toBe('budget-breach')
->and($summary['blockingStatus'])->toBe('non-blocking-warning');
});
it('treats mature fast-feedback budget overruns as blocking when they exceed the CI tolerance', function (): void {
$budgetOutcome = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 216.0);
$summary = TestLaneReport::buildCiSummary(
report: [
'laneId' => 'fast-feedback',
'budgetStatus' => 'within-budget',
'ciContext' => ['workflowId' => 'pr-fast-feedback'],
],
exitCode: 0,
budgetOutcome: $budgetOutcome,
artifactPublicationStatus: ['complete' => true, 'publishedArtifacts' => []],
);
expect($budgetOutcome['budgetStatus'])->toBe('over-budget')
->and($summary['primaryFailureClassId'])->toBe('budget-breach')
->and($summary['blockingStatus'])->toBe('blocking');
});
it('classifies incomplete artifact bundles independently from test and budget status', function (): void {
$summary = TestLaneReport::buildCiSummary(
report: [
'laneId' => 'fast-feedback',
'budgetStatus' => 'within-budget',
'ciContext' => ['workflowId' => 'pr-fast-feedback'],
],
exitCode: 0,
budgetOutcome: null,
artifactPublicationStatus: [
'complete' => false,
'publishedArtifacts' => [],
],
);
expect($summary['primaryFailureClassId'])->toBe('artifact-publication-failure')
->and($summary['blockingStatus'])->toBe('blocking');
});

View File

@ -3,25 +3,84 @@
declare(strict_types=1); declare(strict_types=1);
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps confidence broader than fast-feedback while excluding browser and heavy-governance', function (): void { it('keeps confidence broader than fast-feedback while excluding browser and moved heavy families', function (): void {
$lane = TestLaneManifest::lane('confidence'); $lane = TestLaneManifest::lane('confidence');
$command = TestLaneManifest::buildCommand('confidence'); $command = TestLaneManifest::buildCommand('confidence');
$configurationPath = TestLaneManifest::laneConfigurationPath('confidence');
$configurationContents = (string) file_get_contents(TestLaneManifest::absolutePath($configurationPath));
expect($lane['parallelMode'])->toBe('required') expect($lane['parallelMode'])->toBe('required')
->and($lane['includedFamilies'])->toContain('unit', 'non-browser-feature-integration') ->and($lane['includedFamilies'])->toContain('unit', 'ui-light', 'ui-workflow')
->and($lane['excludedFamilies'])->toContain('browser', 'heavy-governance') ->and($lane['excludedFamilies'])->toContain('browser', 'surface-guard', 'discovery-heavy')
->and($lane['budget']['thresholdSeconds'])->toBeLessThan(TestLaneManifest::fullSuiteBaselineSeconds()) ->and($lane['budget']['thresholdSeconds'])->toBeLessThan(TestLaneManifest::fullSuiteBaselineSeconds())
->and($command)->toContain('--parallel') ->and($command)->toContain('--parallel')
->and($command)->toContain('--testsuite=Unit,Feature') ->and($command)->toContain('--configuration='.$configurationPath)
->and(implode(' ', $command))->toContain('--exclude-group=browser,heavy-governance'); ->and($command)->toContain('--testsuite=Lane')
->and($configurationContents)->toContain('tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php')
->and($configurationContents)->toContain('tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/BaselineActionAuthorizationTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingExceptionRenewalTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingsListFiltersTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingWorkflowRowActionsTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingWorkflowViewActionsTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php')
->and($configurationContents)->not->toContain('tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php');
}); });
it('keeps confidence discovery free of the initial heavy-governance batch', function (): void { it('retains only documented ui-light and selected ui-workflow families in confidence discovery', function (): void {
$files = TestLaneManifest::discoverFiles('confidence'); $files = TestLaneManifest::discoverFiles('confidence');
$confidenceFamilies = TestLaneManifest::familiesByTargetLane('confidence');
expect($files)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php') expect($files)->toContain(
->and($files)->not->toContain('tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php') 'tests/Feature/Filament/BackupSetAdminTenantParityTest.php',
->and($files)->not->toContain('tests/Feature/ProviderConnections/CredentialLeakGuardTest.php') 'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
->and($files)->not->toContain('tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php'); 'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
)
->and($files)->not->toContain(
'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'tests/Feature/Findings/FindingBulkActionsTest.php',
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Guards/ActionSurfaceContractTest.php',
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php',
'tests/Feature/Filament/PanelNavigationSegregationTest.php',
);
foreach ($confidenceFamilies as $family) {
expect($family['classificationId'])->toBeIn(['ui-light', 'ui-workflow'])
->and(trim((string) ($family['confidenceRationale'] ?? '')))->not->toBe('');
}
});
it('keeps the dev confidence budget warning-first so runtime drift stays visible without blocking on budget alone', function (): void {
$profile = TestLaneBudget::enforcementProfile('confidence', 'mainline-push');
$withinVariance = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 470.0);
$overBudget = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 481.0);
expect($profile['enforcementMode'])->toBe('soft-warn')
->and($profile['effectiveThresholdSeconds'])->toBe(480)
->and($withinVariance['budgetStatus'])->toBe('warning')
->and($withinVariance['blockingStatus'])->toBe('non-blocking-warning')
->and($overBudget['budgetStatus'])->toBe('over-budget')
->and($overBudget['blockingStatus'])->toBe('non-blocking-warning');
}); });

View File

@ -3,27 +3,54 @@
declare(strict_types=1); declare(strict_types=1);
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps fast-feedback as the default parallel contributor loop', function (): void { it('keeps fast-feedback as the default parallel contributor loop with explicit heavy exclusions', function (): void {
$lane = TestLaneManifest::lane('fast-feedback'); $lane = TestLaneManifest::lane('fast-feedback');
$command = TestLaneManifest::buildCommand('fast-feedback'); $command = TestLaneManifest::buildCommand('fast-feedback');
$configurationPath = TestLaneManifest::laneConfigurationPath('fast-feedback');
$configurationContents = (string) file_get_contents(TestLaneManifest::absolutePath($configurationPath));
expect($lane['defaultEntryPoint'])->toBeTrue() expect($lane['defaultEntryPoint'])->toBeTrue()
->and($lane['parallelMode'])->toBe('required') ->and($lane['parallelMode'])->toBe('required')
->and($lane['includedFamilies'])->toContain('unit') ->and($lane['includedFamilies'])->toContain('unit', 'ui-light')
->and($lane['excludedFamilies'])->toContain('browser', 'heavy-governance') ->and($lane['excludedFamilies'])->toContain('browser', 'surface-guard', 'discovery-heavy')
->and($lane['budget']['baselineDeltaTargetPercent'])->toBe(50) ->and($lane['budget']['baselineDeltaTargetPercent'])->toBe(50)
->and(TestLaneManifest::commandRef('fast-feedback'))->toBe('test') ->and(TestLaneManifest::commandRef('fast-feedback'))->toBe('test')
->and($command)->toContain('--group=fast-feedback')
->and($command)->toContain('--parallel') ->and($command)->toContain('--parallel')
->and($command)->toContain('--testsuite=Unit,Feature') ->and($command)->toContain('--configuration='.$configurationPath)
->and(implode(' ', $command))->toContain('--exclude-group=browser,heavy-governance'); ->and($command)->toContain('--testsuite=Lane')
->and($configurationContents)->toContain('tests/Feature/Auth/BreakGlassWorkspaceOwnerRecoveryTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($configurationContents)->not->toContain('tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php');
}); });
it('keeps fast-feedback narrower than the broader confidence lane', function (): void { it('keeps fast-feedback narrower than confidence and rejects broad surface or discovery families', function (): void {
$fastTargets = TestLaneManifest::discoverFiles('fast-feedback'); $fastTargets = TestLaneManifest::discoverFiles('fast-feedback');
$confidenceTargets = TestLaneManifest::discoverFiles('confidence'); $confidenceTargets = TestLaneManifest::discoverFiles('confidence');
$validation = TestLaneManifest::validateLanePlacement(
laneId: 'fast-feedback',
filePath: 'tests/Feature/Guards/ActionSurfaceContractTest.php',
);
expect($fastTargets)->not->toBeEmpty() expect($fastTargets)->not->toBeEmpty()
->and(count($fastTargets))->toBeLessThan(count($confidenceTargets)); ->and(count($fastTargets))->toBeLessThan(count($confidenceTargets))
->and($fastTargets)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($validation['valid'])->toBeFalse()
->and($validation['resolvedClassificationId'])->toBe('surface-guard');
}); });
it('treats pull-request fast-feedback budget policy as hard-fail only after the documented CI tolerance is exceeded', function (): void {
$profile = TestLaneBudget::enforcementProfile('fast-feedback', 'pull-request');
$withinTolerance = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 210.0);
$overTolerance = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 216.0);
expect($profile['enforcementMode'])->toBe('hard-fail')
->and($profile['effectiveThresholdSeconds'])->toBe(215)
->and($withinTolerance['budgetStatus'])->toBe('warning')
->and($withinTolerance['blockingStatus'])->toBe('non-blocking-warning')
->and($overTolerance['budgetStatus'])->toBe('over-budget')
->and($overTolerance['blockingStatus'])->toBe('blocking');
});

View File

@ -5,19 +5,31 @@
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
it('excludes browser and initial heavy-governance families from fast-feedback discovery', function (): void { it('excludes browser, discovery-heavy, and surface-guard families from fast-feedback discovery', function (): void {
$files = collect(TestLaneManifest::discoverFiles('fast-feedback')); $files = collect(TestLaneManifest::discoverFiles('fast-feedback'));
$discoveryValidation = TestLaneManifest::validateLanePlacement(
laneId: 'fast-feedback',
filePath: 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
);
expect($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Browser/')))->toBeFalse() expect($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Browser/')))->toBeFalse()
->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Feature/OpsUx/')))->toBeFalse() ->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Feature/OpsUx/')))->toBeFalse()
->and($files)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php') ->and($files)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Architecture/')))->toBeFalse() ->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Architecture/')))->toBeFalse()
->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Deprecation/')))->toBeFalse(); ->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Deprecation/')))->toBeFalse()
->and($discoveryValidation['valid'])->toBeFalse()
->and($discoveryValidation['resolvedClassificationId'])->toBe('discovery-heavy');
}); });
it('keeps fast-feedback focused on the quick-edit families the manifest declares', function (): void { it('keeps fast-feedback focused on the quick-edit families the manifest declares', function (): void {
$files = new Collection(TestLaneManifest::discoverFiles('fast-feedback')); $files = new Collection(TestLaneManifest::discoverFiles('fast-feedback'));
expect($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Unit/')))->toBeTrue() expect($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Unit/')))->toBeTrue()
->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Feature/Guards/')))->toBeTrue(); ->and($files->contains(static fn (string $path): bool => str_starts_with($path, 'tests/Feature/Guards/')))->toBeTrue()
->and($files)->not->toContain(
'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
'tests/Feature/Findings/FindingBulkActionsTest.php',
'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
);
}); });

View File

@ -52,3 +52,60 @@
->and(data_get($stable, 'sharedFixtureSlimmingComparison.status'))->toBe('stable') ->and(data_get($stable, 'sharedFixtureSlimmingComparison.status'))->toBe('stable')
->and(data_get($regressed, 'sharedFixtureSlimmingComparison.status'))->toBe('regressed'); ->and(data_get($regressed, 'sharedFixtureSlimmingComparison.status'))->toBe('regressed');
}); });
it('defines lane, classification, and family budget targets for heavy-governance attribution', function (): void {
$budgetTargets = collect(TestLaneManifest::budgetTargets());
$contract = TestLaneManifest::heavyGovernanceBudgetContract();
$laneBudgetTarget = $budgetTargets
->first(static fn (array $target): bool => $target['targetType'] === 'lane' && $target['targetId'] === 'heavy-governance');
expect($laneBudgetTarget)->not->toBeNull()
->and($contract['summaryThresholdSeconds'])->toBe(300.0)
->and($contract['evaluationThresholdSeconds'])->toBe(200.0)
->and($contract['normalizedThresholdSeconds'])->toBeGreaterThanOrEqual(300.0)
->and($laneBudgetTarget['thresholdSeconds'])->toBe($contract['normalizedThresholdSeconds'])
->and($laneBudgetTarget['lifecycleState'])->toBe($contract['lifecycleState'])
->and($budgetTargets->contains(static fn (array $target): bool => $target['targetType'] === 'classification' && $target['targetId'] === 'surface-guard'))->toBeTrue()
->and($budgetTargets->contains(static fn (array $target): bool => $target['targetType'] === 'classification' && $target['targetId'] === 'discovery-heavy'))->toBeTrue()
->and($budgetTargets->contains(static fn (array $target): bool => $target['targetType'] === 'family' && $target['targetId'] === 'action-surface-contract'))->toBeTrue()
->and($budgetTargets->contains(static fn (array $target): bool => $target['targetType'] === 'family' && $target['targetId'] === 'ops-ux-governance'))->toBeTrue();
});
it('evaluates heavy-governance budgets against named class and family totals', function (): void {
$currentRunContract = TestLaneManifest::heavyGovernanceBudgetContract(110.0);
$durationsByFile = [
'tests/Feature/Guards/ActionSurfaceContractTest.php' => 31.2,
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php' => 17.4,
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php' => 16.1,
'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php' => 9.8,
'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php' => 8.7,
];
$slowestEntries = collect($durationsByFile)
->map(static fn (float $seconds, string $file): array => [
'label' => $file.'::synthetic',
'subject' => $file.'::synthetic',
'filePath' => $file,
'durationSeconds' => $seconds,
'wallClockSeconds' => $seconds,
'laneId' => 'heavy-governance',
])
->values()
->all();
$report = TestLaneReport::buildReport(
laneId: 'heavy-governance',
wallClockSeconds: 110.0,
slowestEntries: $slowestEntries,
durationsByFile: $durationsByFile,
);
expect(collect($report['budgetEvaluations'])->pluck('targetType')->unique()->values()->all())
->toEqualCanonicalizing(['lane', 'classification', 'family'])
->and(collect($report['budgetEvaluations'])->contains(static fn (array $evaluation): bool => $evaluation['targetType'] === 'lane' && $evaluation['targetId'] === 'heavy-governance'))->toBeTrue()
->and(collect($report['budgetEvaluations'])->contains(static fn (array $evaluation): bool => $evaluation['targetType'] === 'classification' && $evaluation['targetId'] === 'surface-guard'))->toBeTrue()
->and(collect($report['budgetEvaluations'])->contains(static fn (array $evaluation): bool => $evaluation['targetType'] === 'family' && $evaluation['targetId'] === 'action-surface-contract'))->toBeTrue()
->and($report['budgetContract']['normalizedThresholdSeconds'])->toBe($currentRunContract['normalizedThresholdSeconds'])
->and($report['budgetOutcome']['decisionStatus'])->toBe($currentRunContract['decisionStatus']);
});

View File

@ -4,22 +4,107 @@
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('routes the initial architecture, deprecation, ops-ux, and action-surface batch into heavy-governance', function (): void { it('routes escalated workflow, discovery-heavy, and broad surface-guard families into heavy-governance', function (): void {
$lane = TestLaneManifest::lane('heavy-governance'); $lane = TestLaneManifest::lane('heavy-governance');
$files = new Collection(TestLaneManifest::discoverFiles('heavy-governance')); $files = new Collection(TestLaneManifest::discoverFiles('heavy-governance'));
expect($lane['includedFamilies'])->toContain('architecture-governance', 'ops-ux') expect($lane['includedFamilies'])->toContain('ui-workflow', 'surface-guard', 'discovery-heavy')
->and($lane['selectors']['includeGroups'])->toContain('heavy-governance') ->and($files)->toContain(
->and($files)->toContain('tests/Architecture/PlatformVocabularyBoundaryGuardTest.php') 'tests/Architecture/PlatformVocabularyBoundaryGuardTest.php',
->and($files)->toContain('tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php') 'tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php',
->and($files)->toContain('tests/Deprecation/IsPlatformSuperadminDeprecationTest.php') 'tests/Deprecation/IsPlatformSuperadminDeprecationTest.php',
->and($files)->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php'); 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'tests/Feature/Findings/FindingBulkActionsTest.php',
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
'tests/Feature/Guards/ActionSurfaceContractTest.php',
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php',
'tests/Feature/Filament/PanelNavigationSegregationTest.php',
);
}); });
it('keeps the heavy-governance command group-driven for intentionally expensive families', function (): void { it('keeps the heavy-governance command config-driven and free of retained confidence workflows', function (): void {
$command = TestLaneManifest::buildCommand('heavy-governance'); $command = TestLaneManifest::buildCommand('heavy-governance');
$files = TestLaneManifest::discoverFiles('heavy-governance');
$configurationPath = TestLaneManifest::laneConfigurationPath('heavy-governance');
$configurationContents = (string) file_get_contents(TestLaneManifest::absolutePath($configurationPath));
expect($command)->toContain('--group=heavy-governance') expect(TestLaneManifest::commandRef('heavy-governance'))->toBe('test:heavy')
->and(TestLaneManifest::commandRef('heavy-governance'))->toBe('test:heavy'); ->and($command)->toContain('--configuration='.$configurationPath)
->and($command)->toContain('--testsuite=Lane')
->and($configurationContents)->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and($configurationContents)->toContain('tests/Feature/Filament/BaselineActionAuthorizationTest.php')
->and($configurationContents)->toContain('tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php')
->and($configurationContents)->toContain('tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php')
->and($configurationContents)->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and($configurationContents)->toContain('tests/Feature/Findings/FindingExceptionRenewalTest.php')
->and($configurationContents)->toContain('tests/Feature/Findings/FindingsListFiltersTest.php')
->and($configurationContents)->toContain('tests/Feature/Findings/FindingWorkflowRowActionsTest.php')
->and($configurationContents)->toContain('tests/Feature/Findings/FindingWorkflowViewActionsTest.php')
->and($configurationContents)->toContain('tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php')
->and($configurationContents)->toContain('tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php')
->and($configurationContents)->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($configurationContents)->toContain('tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php')
->and($files)->not->toContain(
'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
'tests/Feature/Filament/BackupSetAdminTenantParityTest.php',
);
});
it('keeps heavy-governance hotspot ownership honest across slimmed, retained, and follow-up families', function (): void {
$inventory = collect(TestLaneManifest::heavyGovernanceHotspotInventory())->keyBy('familyId');
$decomposition = collect(TestLaneManifest::heavyGovernanceDecompositionRecords())->keyBy('familyId');
$decisions = collect(TestLaneManifest::heavyGovernanceSlimmingDecisions())->keyBy('familyId');
$outcome = TestLaneManifest::heavyGovernanceBudgetOutcome();
expect($inventory->keys()->all())->toEqual([
'baseline-profile-start-surfaces',
'action-surface-contract',
'ops-ux-governance',
'findings-workflow-surfaces',
'finding-bulk-actions-workflow',
'workspace-settings-slice-management',
])
->and($inventory->get('baseline-profile-start-surfaces')['status'])->toBe('slimmed')
->and($inventory->get('findings-workflow-surfaces')['status'])->toBe('slimmed')
->and($inventory->get('finding-bulk-actions-workflow')['status'])->toBe('slimmed')
->and($inventory->get('action-surface-contract')['status'])->toBe('retained')
->and($inventory->get('ops-ux-governance')['status'])->toBe('retained')
->and($inventory->get('workspace-settings-slice-management')['status'])->toBe('follow-up')
->and($decomposition->get('finding-bulk-actions-workflow')['recommendedAction'])->toBe('narrow-assertions')
->and($decisions->get('finding-bulk-actions-workflow')['decisionType'])->toBe('trim-duplicate-work')
->and($decisions->get('workspace-settings-slice-management')['decisionType'])->toBe('follow-up')
->and($outcome['remainingOpenFamilies'])->toEqualCanonicalizing([
'action-surface-contract',
'ops-ux-governance',
'workspace-settings-slice-management',
]);
});
it('keeps heavy-governance manual runs warning-first and scheduled runs trend-only while schedules mature', function (): void {
$manualProfile = TestLaneBudget::enforcementProfile('heavy-governance', 'manual');
$scheduledProfile = TestLaneBudget::enforcementProfile('heavy-governance', 'scheduled');
$manualEvaluation = TestLaneBudget::evaluateLaneForTrigger('heavy-governance', 'manual', (float) $manualProfile['effectiveThresholdSeconds'] + 1.0);
$scheduledEvaluation = TestLaneBudget::evaluateLaneForTrigger('heavy-governance', 'scheduled', (float) $scheduledProfile['effectiveThresholdSeconds'] + 1.0);
expect($manualProfile['thresholdSource'])->toBe('governance-contract')
->and($manualProfile['enforcementMode'])->toBe('soft-warn')
->and($scheduledProfile['enforcementMode'])->toBe('trend-only')
->and($manualEvaluation['blockingStatus'])->toBe('non-blocking-warning')
->and($scheduledEvaluation['blockingStatus'])->toBe('informational');
}); });

View File

@ -4,6 +4,16 @@
use App\Services\Operations\OperationLifecyclePolicyValidator; use App\Services\Operations\OperationLifecyclePolicyValidator;
use Tests\Support\OpsUx\SourceFileScanner; use Tests\Support\OpsUx\SourceFileScanner;
use Tests\Support\TestLaneManifest;
it('keeps ops ux governance retained in the heavy-governance inventory', function (): void {
$inventoryRecord = collect(TestLaneManifest::heavyGovernanceHotspotInventory())
->firstWhere('familyId', 'ops-ux-governance');
expect($inventoryRecord)->not->toBeNull()
->and($inventoryRecord['classificationId'])->toBe('surface-guard')
->and($inventoryRecord['status'])->toBe('retained');
});
it('keeps lifecycle bridge ownership and initiator-null notification discipline intact', function (): void { it('keeps lifecycle bridge ownership and initiator-null notification discipline intact', function (): void {
$validator = app(OperationLifecyclePolicyValidator::class); $validator = app(OperationLifecyclePolicyValidator::class);

View File

@ -3,15 +3,76 @@
declare(strict_types=1); declare(strict_types=1);
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneReport;
it('keeps profiling serial and artifact-rich for slow-test drift analysis', function (): void { it('keeps profiling serial and artifact-rich for slow-test drift analysis', function (): void {
$lane = TestLaneManifest::lane('profiling'); $lane = TestLaneManifest::lane('profiling');
$command = TestLaneManifest::buildCommand('profiling'); $command = TestLaneManifest::buildCommand('profiling');
$configurationPath = TestLaneManifest::laneConfigurationPath('profiling');
$configurationContents = (string) file_get_contents(TestLaneManifest::absolutePath($configurationPath));
expect($lane['governanceClass'])->toBe('support') expect($lane['governanceClass'])->toBe('support')
->and($lane['parallelMode'])->toBe('forbidden') ->and($lane['parallelMode'])->toBe('forbidden')
->and($lane['artifacts'])->toContain('profile-top', 'junit-xml', 'summary', 'budget-report') ->and($lane['artifacts'])->toContain('profile-top', 'junit-xml', 'summary', 'budget-report')
->and($command)->toContain('--profile') ->and($command)->toContain('--profile')
->and($command)->not->toContain('--parallel') ->and($command)->not->toContain('--parallel')
->and(implode(' ', $command))->toContain('--exclude-group=browser'); ->and($command)->toContain('--configuration='.$configurationPath)
->and($configurationContents)->toContain('tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php')
->and($configurationContents)->not->toContain('tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php');
});
it('builds top 10 attribution-rich profiling reports for mixed workflow and governance files', function (): void {
$durationsByFile = [
'tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php' => 22.4,
'tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php' => 20.1,
'tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php' => 18.5,
'tests/Feature/Guards/ActionSurfaceContractTest.php' => 17.3,
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php' => 16.2,
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php' => 15.1,
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php' => 13.7,
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php' => 12.8,
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php' => 11.4,
'tests/Feature/Filament/BackupSetAdminTenantParityTest.php' => 9.6,
];
$slowestEntries = collect($durationsByFile)
->map(static fn (float $seconds, string $file): array => [
'label' => $file.'::synthetic',
'subject' => $file.'::synthetic',
'filePath' => $file,
'durationSeconds' => $seconds,
'wallClockSeconds' => $seconds,
'laneId' => 'profiling',
])
->values()
->all();
$report = TestLaneReport::buildReport(
laneId: 'profiling',
wallClockSeconds: 181.7,
slowestEntries: $slowestEntries,
durationsByFile: $durationsByFile,
);
expect($report['slowestEntries'])->toHaveCount(10)
->and($report['slowestEntries'][0]['wallClockSeconds'])->toBeGreaterThanOrEqual($report['slowestEntries'][1]['wallClockSeconds'])
->and(collect($report['classificationAttribution'])->pluck('classificationId')->all())
->toContain('ui-light', 'ui-workflow', 'surface-guard', 'discovery-heavy')
->and(collect($report['familyAttribution'])->pluck('familyId')->all())
->toContain('baseline-compare-matrix-workflow', 'action-surface-contract', 'backup-set-admin-tenant-parity');
});
it('keeps heavy-governance snapshot artifact paths rooted in the canonical lane directory', function (): void {
$report = TestLaneReport::buildReport(
laneId: 'heavy-governance',
wallClockSeconds: 140.0,
slowestEntries: [],
durationsByFile: [],
);
expect($report['budgetSnapshots'])->toHaveCount(2)
->and($report['budgetSnapshots'][0]['artifactPaths']['summary'])->toStartWith('storage/logs/test-lanes/heavy-governance-')
->and($report['budgetSnapshots'][1]['artifactPaths']['summary'])->toBe('storage/logs/test-lanes/heavy-governance-latest.summary.md')
->and($report['budgetSnapshots'][1]['artifactPaths']['budget'])->toBe('storage/logs/test-lanes/heavy-governance-latest.budget.json')
->and($report['budgetSnapshots'][1]['artifactPaths']['report'])->toBe('storage/logs/test-lanes/heavy-governance-latest.report.json');
}); });

View File

@ -2,13 +2,34 @@
declare(strict_types=1); declare(strict_types=1);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneReport; use Tests\Support\TestLaneReport;
function heavyGovernanceSyntheticHotspots(): array
{
return [
['file' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php', 'seconds' => 22.4],
['file' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php', 'seconds' => 21.1],
['file' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php', 'seconds' => 19.5],
['file' => 'tests/Feature/Findings/FindingBulkActionsTest.php', 'seconds' => 18.2],
['file' => 'tests/Feature/Findings/FindingWorkflowRowActionsTest.php', 'seconds' => 14.8],
['file' => 'tests/Feature/Findings/FindingWorkflowViewActionsTest.php', 'seconds' => 13.6],
['file' => 'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php', 'seconds' => 12.7],
['file' => 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'seconds' => 11.9],
['file' => 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'seconds' => 10.4],
['file' => 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php', 'seconds' => 4.2],
];
}
it('keeps lane artifact paths app-root relative under storage/logs/test-lanes', function (): void { it('keeps lane artifact paths app-root relative under storage/logs/test-lanes', function (): void {
$artifacts = TestLaneReport::artifactPaths('fast-feedback'); $artifacts = TestLaneReport::artifactPaths('fast-feedback');
$artifactContract = TestLaneManifest::artifactPublicationContract('fast-feedback');
expect($artifacts)->toHaveKeys(['junit', 'summary', 'budget', 'report', 'profile']); expect($artifacts)->toHaveKeys(['junit', 'summary', 'budget', 'report', 'profile']);
expect($artifactContract['requiredFiles'])->toEqualCanonicalizing(['summary.md', 'budget.json', 'report.json', 'junit.xml'])
->and($artifactContract['stagedNamePattern'])->toBe('{laneId}.{artifactFile}');
foreach (array_values($artifacts) as $relativePath) { foreach (array_values($artifacts) as $relativePath) {
expect($relativePath)->toStartWith('storage/logs/test-lanes/'); expect($relativePath)->toStartWith('storage/logs/test-lanes/');
} }
@ -22,6 +43,131 @@
->and((string) file_get_contents($gitignore))->toContain('!.gitignore'); ->and((string) file_get_contents($gitignore))->toContain('!.gitignore');
}); });
it('publishes heavy attribution, contract, and coverage payloads under the canonical artifact root', function (): void {
$durationsByFile = collect(heavyGovernanceSyntheticHotspots())
->mapWithKeys(static fn (array $entry): array => [$entry['file'] => $entry['seconds']])
->all();
$slowestEntries = collect(heavyGovernanceSyntheticHotspots())
->map(static fn (array $entry): array => [
'label' => $entry['file'].'::synthetic',
'subject' => $entry['file'].'::synthetic',
'filePath' => $entry['file'],
'durationSeconds' => $entry['seconds'],
'wallClockSeconds' => $entry['seconds'],
'laneId' => 'heavy-governance',
])
->values()
->all();
$report = TestLaneReport::buildReport(
laneId: 'heavy-governance',
wallClockSeconds: 118.4,
slowestEntries: $slowestEntries,
durationsByFile: $durationsByFile,
);
expect($report['artifactDirectory'])->toBe('storage/logs/test-lanes')
->and($report['slowestEntries'])->toHaveCount(10)
->and($report)->toHaveKeys([
'artifactPublicationContract',
'knownWorkflowProfiles',
'failureClasses',
'budgetContract',
'hotspotInventory',
'decompositionRecords',
'slimmingDecisions',
'authorGuidance',
'inventoryCoverage',
'budgetSnapshots',
'budgetOutcome',
'remainingOpenFamilies',
'stabilizedFamilies',
])
->and(collect($report['classificationAttribution'])->pluck('classificationId')->all())
->toContain('ui-workflow', 'surface-guard', 'discovery-heavy')
->and(collect($report['familyAttribution'])->pluck('familyId')->all())
->toContain(
'baseline-profile-start-surfaces',
'findings-workflow-surfaces',
'finding-bulk-actions-workflow',
'workspace-settings-slice-management',
'action-surface-contract',
'ops-ux-governance',
)
->and(collect($report['budgetEvaluations'])->pluck('targetType')->unique()->values()->all())
->toEqualCanonicalizing(['lane', 'classification', 'family'])
->and($report['familyBudgetEvaluations'])->not->toBeEmpty()
->and($report['inventoryCoverage']['meetsInclusionRule'])->toBeTrue()
->and($report['inventoryCoverage']['inventoryFamilyCount'])->toBe(6)
->and($report['budgetSnapshots'])->toHaveCount(2)
->and($report['budgetOutcome'])->toHaveKeys([
'decisionStatus',
'finalThresholdSeconds',
'remainingOpenFamilies',
'followUpDebt',
])
->and($report['knownWorkflowProfiles'])->toContain('heavy-governance-manual', 'heavy-governance-scheduled');
});
it('stages deterministic CI artifact bundles from the canonical lane outputs', function (): void {
$artifactDirectory = 'storage/logs/test-lanes/contract-stage-test';
$stagingDirectory = base_path('storage/logs/test-lanes/contract-stage-test/staged');
$report = TestLaneReport::buildReport(
laneId: 'fast-feedback',
wallClockSeconds: 182.4,
slowestEntries: [],
durationsByFile: [],
artifactDirectory: $artifactDirectory,
ciContext: [
'workflowId' => 'pr-fast-feedback',
'triggerClass' => 'pull-request',
'entryPointResolved' => true,
'workflowLaneMatched' => true,
],
);
TestLaneReport::writeArtifacts(
laneId: 'fast-feedback',
report: $report,
profileOutput: null,
artifactDirectory: $artifactDirectory,
exitCode: 0,
);
$artifactPaths = TestLaneReport::artifactPaths('fast-feedback', $artifactDirectory);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['junit']),
<<<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="fast-feedback" tests="1" assertions="1" errors="0" failures="0" skipped="0" time="0.1">
<testcase name="synthetic" file="tests/Feature/Guards/TestLaneArtifactsContractTest.php" time="0.1" />
</testsuite>
</testsuites>
XML,
);
$stagingResult = TestLaneReport::stageArtifacts(
laneId: 'fast-feedback',
stagingDirectory: $stagingDirectory,
artifactDirectory: $artifactDirectory,
);
expect($stagingResult['complete'])->toBeTrue()
->and(collect($stagingResult['stagedArtifacts'])->pluck('artifactType')->all())
->toEqualCanonicalizing(['summary.md', 'budget.json', 'report.json', 'junit.xml'])
->and(collect($stagingResult['stagedArtifacts'])->pluck('relativePath')->all())
->toContain(
$stagingDirectory.'/fast-feedback.summary.md',
$stagingDirectory.'/fast-feedback.budget.json',
$stagingDirectory.'/fast-feedback.report.json',
$stagingDirectory.'/fast-feedback.junit.xml',
);
});
it('publishes the shared fixture slimming comparison only for the governed standard lanes', function (): void { it('publishes the shared fixture slimming comparison only for the governed standard lanes', function (): void {
$fastFeedback = TestLaneReport::buildReport( $fastFeedback = TestLaneReport::buildReport(
laneId: 'fast-feedback', laneId: 'fast-feedback',

View File

@ -16,6 +16,11 @@
'test:heavy', 'test:heavy',
'test:profile', 'test:profile',
'test:junit', 'test:junit',
'test:report',
'test:report:confidence',
'test:report:browser',
'test:report:heavy',
'test:report:profile',
'sail:test', 'sail:test',
]) ])
->and(TestLaneManifest::commandRef('fast-feedback'))->toBe('test') ->and(TestLaneManifest::commandRef('fast-feedback'))->toBe('test')
@ -28,16 +33,91 @@
it('keeps the host-side lane runner and report scripts checked in at repo root', function (): void { it('keeps the host-side lane runner and report scripts checked in at repo root', function (): void {
expect(file_exists(repo_path('scripts/platform-test-lane')))->toBeTrue() expect(file_exists(repo_path('scripts/platform-test-lane')))->toBeTrue()
->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue(); ->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue()
->and(file_exists(repo_path('scripts/platform-test-artifacts')))->toBeTrue();
});
it('keeps heavy-governance baseline capture support inside the checked-in wrappers', function (): void {
$laneRunner = (string) file_get_contents(repo_path('scripts/platform-test-lane'));
$reportRunner = (string) file_get_contents(repo_path('scripts/platform-test-report'));
expect($laneRunner)->toContain('--capture-baseline', 'copy_heavy_baseline_artifacts', 'heavy-governance-baseline.${suffix}')
->and($reportRunner)->toContain('--capture-baseline', 'copy_heavy_baseline_artifacts', 'heavy-governance-baseline.${suffix}');
});
it('avoids expanding an empty forwarded-argument array in the lane runner', function (): void {
$laneRunner = (string) file_get_contents(repo_path('scripts/platform-test-lane'));
expect($laneRunner)
->toContain('if [[ ${#remaining_args[@]} -gt 0 ]]; then')
->and($laneRunner)->toContain('./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "${remaining_args[@]}"')
->and($laneRunner)->toContain('./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"')
->and($laneRunner)->toContain('--workflow-id=')
->and($laneRunner)->toContain('--trigger-class=');
});
it('keeps CI workflow validation and wrong-lane drift detection in the checked-in manifest', function (): void {
$validExecution = TestLaneManifest::validateWorkflowExecution('pr-fast-feedback', 'fast-feedback');
$wrongLaneExecution = TestLaneManifest::validateWorkflowExecution('pr-fast-feedback', 'confidence');
expect($validExecution['valid'])->toBeTrue()
->and($validExecution['primaryFailureClassId'])->toBeNull()
->and($wrongLaneExecution['valid'])->toBeFalse()
->and($wrongLaneExecution['workflowLaneMatched'])->toBeFalse()
->and($wrongLaneExecution['primaryFailureClassId'])->toBe('wrapper-failure');
});
it('degrades invalid CI workflow context to wrapper-failure metadata instead of crashing the lane context lookup', function (): void {
$originalWorkflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID');
$originalTriggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS');
putenv('TENANTATLAS_CI_WORKFLOW_ID=missing-workflow-profile');
putenv('TENANTATLAS_CI_TRIGGER_CLASS=pull-request');
try {
$context = TestLaneManifest::currentCiContext('fast-feedback');
expect($context['entryPointResolved'])->toBeFalse()
->and($context['workflowLaneMatched'])->toBeFalse()
->and($context['primaryFailureClassId'])->toBe('wrapper-failure')
->and($context['unresolvedEntryPoints'])->toContain('workflow-profile');
} finally {
putenv($originalWorkflowId === false ? 'TENANTATLAS_CI_WORKFLOW_ID' : sprintf('TENANTATLAS_CI_WORKFLOW_ID=%s', $originalWorkflowId));
putenv($originalTriggerClass === false ? 'TENANTATLAS_CI_TRIGGER_CLASS' : sprintf('TENANTATLAS_CI_TRIGGER_CLASS=%s', $originalTriggerClass));
}
}); });
it('routes the foundational lane commands through stable artisan arguments', function (): void { it('routes the foundational lane commands through stable artisan arguments', function (): void {
$fastFeedbackConfig = TestLaneManifest::laneConfigurationPath('fast-feedback');
$fastFeedbackContents = (string) file_get_contents(TestLaneManifest::absolutePath($fastFeedbackConfig));
$confidenceConfig = TestLaneManifest::laneConfigurationPath('confidence');
$confidenceContents = (string) file_get_contents(TestLaneManifest::absolutePath($confidenceConfig));
$browserConfig = TestLaneManifest::laneConfigurationPath('browser');
$browserContents = (string) file_get_contents(TestLaneManifest::absolutePath($browserConfig));
$heavyConfig = TestLaneManifest::laneConfigurationPath('heavy-governance');
$heavyContents = (string) file_get_contents(TestLaneManifest::absolutePath($heavyConfig));
expect(TestLaneManifest::buildCommand('fast-feedback'))->toContain('--parallel') expect(TestLaneManifest::buildCommand('fast-feedback'))->toContain('--parallel')
->and(TestLaneManifest::buildCommand('fast-feedback'))->toContain('--group=fast-feedback') ->and(TestLaneManifest::buildCommand('fast-feedback'))->toContain('--configuration='.$fastFeedbackConfig)
->and(TestLaneManifest::buildCommand('fast-feedback'))->toContain('--testsuite=Unit,Feature') ->and($fastFeedbackContents)->not->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and(TestLaneManifest::buildCommand('confidence'))->toContain('--testsuite=Unit,Feature') ->and($fastFeedbackContents)->not->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and(TestLaneManifest::buildCommand('browser'))->toContain('--group=browser') ->and($fastFeedbackContents)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and(TestLaneManifest::buildCommand('browser'))->toContain('--testsuite=Browser') ->and(TestLaneManifest::buildCommand('confidence'))->toContain('--configuration='.$confidenceConfig)
->and(TestLaneManifest::buildCommand('heavy-governance'))->toContain('--group=heavy-governance') ->and($confidenceContents)->toContain('tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Filament/BaselineActionAuthorizationTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and($confidenceContents)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and(TestLaneManifest::buildCommand('browser'))->toContain('--configuration='.$browserConfig)
->and($browserContents)->toContain('tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php')
->and(TestLaneManifest::buildCommand('heavy-governance'))->toContain('--configuration='.$heavyConfig)
->and($heavyContents)->toContain('tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php')
->and($heavyContents)->toContain('tests/Feature/Filament/BaselineActionAuthorizationTest.php')
->and($heavyContents)->toContain('tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php')
->and($heavyContents)->toContain('tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php')
->and($heavyContents)->toContain('tests/Feature/Findings/FindingBulkActionsTest.php')
->and($heavyContents)->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and(TestLaneManifest::buildCommand('junit'))->toContain('--parallel'); ->and(TestLaneManifest::buildCommand('junit'))->toContain('--parallel');
}); });

View File

@ -4,7 +4,7 @@
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
it('declares the six checked-in lanes with a single fast-feedback default', function (): void { it('declares the six checked-in lanes with a single fast-feedback default and the spec 208 plus 209 metadata surfaces', function (): void {
$manifest = TestLaneManifest::manifest(); $manifest = TestLaneManifest::manifest();
$laneIds = array_column($manifest['lanes'], 'id'); $laneIds = array_column($manifest['lanes'], 'id');
$defaultLanes = array_values(array_filter( $defaultLanes = array_values(array_filter(
@ -12,8 +12,31 @@
static fn (array $lane): bool => $lane['defaultEntryPoint'] === true, static fn (array $lane): bool => $lane['defaultEntryPoint'] === true,
)); ));
expect($manifest['version'])->toBe(1) expect($manifest['version'])->toBe(2)
->and($manifest['artifactDirectory'])->toBe('storage/logs/test-lanes') ->and($manifest['artifactDirectory'])->toBe('storage/logs/test-lanes')
->and($manifest['mainlineBranch'])->toBe('dev')
->and($manifest)->toHaveKeys([
'classifications',
'families',
'mixedFileResolutions',
'placementRules',
'driftGuards',
'budgetTargets',
'lanes',
'workflowProfiles',
'laneBindings',
'budgetEnforcementProfiles',
'artifactPublicationContracts',
'failureClasses',
'familyBudgets',
'heavyGovernanceBudgetContract',
'heavyGovernanceHotspotInventory',
'heavyGovernanceDecompositionRecords',
'heavyGovernanceSlimmingDecisions',
'heavyGovernanceBudgetSnapshots',
'heavyGovernanceBudgetOutcome',
'heavyGovernanceAuthorGuidance',
])
->and($laneIds)->toEqualCanonicalizing([ ->and($laneIds)->toEqualCanonicalizing([
'fast-feedback', 'fast-feedback',
'confidence', 'confidence',
@ -26,7 +49,38 @@
->and($defaultLanes[0]['id'])->toBe('fast-feedback'); ->and($defaultLanes[0]['id'])->toBe('fast-feedback');
}); });
it('keeps every lane declaration populated with governance metadata, selectors, and budgets', function (): void { it('publishes the CI workflow matrix, lane bindings, artifact contracts, and failure classes from repo truth', function (): void {
$workflowProfiles = collect(TestLaneManifest::workflowProfiles())->keyBy('workflowId');
$laneBindings = collect(TestLaneManifest::laneBindings())->keyBy('laneId');
$artifactContracts = collect(TestLaneManifest::artifactPublicationContracts())->keyBy('laneId');
$failureClasses = collect(TestLaneManifest::failureClasses())->keyBy('failureClassId');
expect($workflowProfiles->keys()->all())->toEqualCanonicalizing([
'pr-fast-feedback',
'main-confidence',
'heavy-governance-manual',
'heavy-governance-scheduled',
'browser-manual',
'browser-scheduled',
])
->and($workflowProfiles->get('pr-fast-feedback')['laneBindings'])->toBe(['fast-feedback'])
->and($workflowProfiles->get('main-confidence')['branchFilters'])->toBe(['dev'])
->and($workflowProfiles->get('heavy-governance-scheduled')['scheduleCron'])->toBe('17 4 * * 1-5')
->and($workflowProfiles->get('browser-scheduled')['scheduleCron'])->toBe('43 4 * * 1-5')
->and($laneBindings->get('fast-feedback')['executionWrapper'])->toBe('scripts/platform-test-lane')
->and($laneBindings->get('confidence')['requiredArtifacts'])->toEqualCanonicalizing(['summary.md', 'budget.json', 'report.json', 'junit.xml'])
->and($artifactContracts->get('fast-feedback')['retentionClass'])->toBe('pr-short')
->and($artifactContracts->get('browser')['uploadGroupName'])->toBe('browser-artifacts')
->and($failureClasses->keys()->all())->toEqualCanonicalizing([
'test-failure',
'wrapper-failure',
'budget-breach',
'artifact-publication-failure',
'infrastructure-failure',
]);
});
it('keeps every lane declaration populated with governance metadata, selectors, and segmented family expectations', function (): void {
foreach (TestLaneManifest::manifest()['lanes'] as $lane) { foreach (TestLaneManifest::manifest()['lanes'] as $lane) {
expect(trim($lane['description']))->not->toBe('') expect(trim($lane['description']))->not->toBe('')
->and(trim($lane['intendedAudience']))->not->toBe('') ->and(trim($lane['intendedAudience']))->not->toBe('')
@ -52,13 +106,90 @@
expect($selectors)->toHaveKey($selectorKey); expect($selectors)->toHaveKey($selectorKey);
} }
} }
expect(TestLaneManifest::lane('confidence')['includedFamilies'])->toContain('ui-light', 'ui-workflow')
->and(TestLaneManifest::lane('confidence')['excludedFamilies'])->toContain('surface-guard', 'discovery-heavy')
->and(TestLaneManifest::lane('heavy-governance')['includedFamilies'])->toContain('surface-guard', 'discovery-heavy');
}); });
it('seeds at least one initial heavy family budget beside the lane-level budgets', function (): void { it('exposes the spec 208 classification catalog and seeded family inventory with required metadata', function (): void {
$classifications = collect(TestLaneManifest::classifications())->keyBy('classificationId');
$families = collect(TestLaneManifest::families())->keyBy('familyId');
expect($classifications->keys()->all())->toEqualCanonicalizing([
'ui-light',
'ui-workflow',
'surface-guard',
'discovery-heavy',
'browser',
])
->and($classifications->get('browser')['allowedLaneIds'])->toBe(['browser'])
->and($classifications->get('surface-guard')['defaultLaneId'])->toBe('heavy-governance')
->and($classifications->get('discovery-heavy')['forbiddenLaneIds'])->toContain('fast-feedback', 'confidence')
->and($families->has('baseline-profile-start-surfaces'))->toBeTrue()
->and($families->has('findings-workflow-surfaces'))->toBeTrue()
->and($families->has('finding-bulk-actions-workflow'))->toBeTrue()
->and($families->has('drift-bulk-triage-all-matching'))->toBeTrue()
->and($families->has('policy-resource-admin-search-parity'))->toBeTrue()
->and($families->has('workspace-only-admin-surface-independence'))->toBeTrue()
->and($families->has('workspace-settings-slice-management'))->toBeTrue()
->and($families->has('baseline-compare-matrix-workflow'))->toBeTrue()
->and($families->has('browser-smoke'))->toBeTrue();
foreach (TestLaneManifest::families() as $family) {
expect(trim($family['purpose']))->not->toBe('')
->and(trim($family['currentLaneId']))->not->toBe('')
->and(trim($family['targetLaneId']))->not->toBe('')
->and($family['selectors'])->not->toBeEmpty()
->and($family['hotspotFiles'])->not->toBeEmpty()
->and(trim($family['validationStatus']))->not->toBe('');
if ($family['targetLaneId'] === 'confidence') {
expect(trim((string) ($family['confidenceRationale'] ?? '')))->not->toBe('');
}
}
});
it('keeps family budgets derived from the generic budget targets for report consumers', function (): void {
$familyBudgets = TestLaneManifest::familyBudgets(); $familyBudgets = TestLaneManifest::familyBudgets();
expect($familyBudgets)->not->toBeEmpty() expect($familyBudgets)->not->toBeEmpty()
->and($familyBudgets[0]['familyId'])->toBeString() ->and($familyBudgets[0])->toHaveKeys(['familyId', 'targetType', 'targetId', 'selectors', 'thresholdSeconds'])
->and($familyBudgets[0]['selectors'])->not->toBeEmpty() ->and(collect($familyBudgets)->pluck('familyId')->all())
->and($familyBudgets[0]['thresholdSeconds'])->toBeGreaterThan(0); ->toContain('action-surface-contract', 'browser-smoke', 'baseline-compare-matrix-workflow', 'baseline-profile-start-surfaces', 'drift-bulk-triage-all-matching', 'finding-bulk-actions-workflow', 'findings-workflow-surfaces', 'workspace-only-admin-surface-independence', 'workspace-settings-slice-management');
});
it('publishes the heavy-governance contract, inventory, and guidance surfaces needed for honest rerun review', function (): void {
$contract = TestLaneManifest::heavyGovernanceBudgetContract();
$inventory = collect(TestLaneManifest::heavyGovernanceHotspotInventory());
expect($contract['summaryThresholdSeconds'])->toBe(300.0)
->and($contract['evaluationThresholdSeconds'])->toBe(200.0)
->and($contract['normalizedThresholdSeconds'])->toBeGreaterThanOrEqual(300.0)
->and($contract['decisionStatus'])->toBeIn(['recovered', 'recalibrated'])
->and($inventory)->toHaveCount(6)
->and($inventory->pluck('familyId')->all())->toEqual([
'baseline-profile-start-surfaces',
'action-surface-contract',
'ops-ux-governance',
'findings-workflow-surfaces',
'finding-bulk-actions-workflow',
'workspace-settings-slice-management',
])
->and(collect(TestLaneManifest::heavyGovernanceBudgetSnapshots()))->toHaveCount(2)
->and(TestLaneManifest::heavyGovernanceBudgetOutcome())->toHaveKeys([
'decisionStatus',
'finalThresholdSeconds',
'deltaSeconds',
'remainingOpenFamilies',
'followUpDebt',
])
->and(collect(TestLaneManifest::heavyGovernanceAuthorGuidance())->pluck('ruleId')->all())
->toEqualCanonicalizing([
'heavy-family-reuse-before-creation',
'heavy-family-create-only-for-new-trust',
'split-discovery-workflow-surface-concerns',
'retain-intentional-heavy-depth-explicitly',
'record-helper-or-fixture-residuals',
]);
}); });

View File

@ -4,16 +4,97 @@
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
it('keeps the first audited heavy batch out of the confidence lane and inside heavy-governance', function (): void { it('declares the spec 208 five-class catalog with the expected lane semantics', function (): void {
$classifications = collect(TestLaneManifest::classifications())->keyBy('classificationId');
expect($classifications->keys()->all())->toEqualCanonicalizing([
'ui-light',
'ui-workflow',
'surface-guard',
'discovery-heavy',
'browser',
])
->and($classifications->get('ui-light')['allowedLaneIds'])->toContain('fast-feedback', 'confidence')
->and($classifications->get('ui-workflow')['defaultLaneId'])->toBe('confidence')
->and($classifications->get('surface-guard')['forbiddenLaneIds'])->toContain('fast-feedback', 'confidence')
->and($classifications->get('discovery-heavy')['defaultLaneId'])->toBe('heavy-governance')
->and($classifications->get('browser')['allowedLaneIds'])->toBe(['browser']);
});
it('keeps the seeded heavy batch out of confidence and inside heavy-governance', function (): void {
$confidenceFiles = TestLaneManifest::discoverFiles('confidence'); $confidenceFiles = TestLaneManifest::discoverFiles('confidence');
$heavyFiles = TestLaneManifest::discoverFiles('heavy-governance'); $heavyFiles = TestLaneManifest::discoverFiles('heavy-governance');
expect($confidenceFiles)->not->toContain('tests/Architecture/PlatformVocabularyBoundaryGuardTest.php') expect($confidenceFiles)->not->toContain(
->and($confidenceFiles)->not->toContain('tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php') 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
->and($confidenceFiles)->not->toContain('tests/Deprecation/IsPlatformSuperadminDeprecationTest.php') 'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
->and($confidenceFiles)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php') 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
->and($heavyFiles)->toContain('tests/Architecture/PlatformVocabularyBoundaryGuardTest.php') 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
->and($heavyFiles)->toContain('tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php') 'tests/Feature/Findings/FindingBulkActionsTest.php',
->and($heavyFiles)->toContain('tests/Deprecation/IsPlatformSuperadminDeprecationTest.php') 'tests/Feature/Findings/FindingExceptionRenewalTest.php',
->and($heavyFiles)->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php'); 'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
'tests/Feature/Guards/ActionSurfaceContractTest.php',
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php',
'tests/Feature/Filament/PanelNavigationSegregationTest.php',
)
->and($heavyFiles)->toContain(
'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'tests/Feature/Findings/FindingBulkActionsTest.php',
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
'tests/Feature/Guards/ActionSurfaceContractTest.php',
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php',
'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php',
'tests/Feature/Filament/PanelNavigationSegregationTest.php',
);
});
it('describes mixed-file and wrong-lane placement with actionable output', function (): void {
$validation = TestLaneManifest::validateLanePlacement(
laneId: 'confidence',
filePath: 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php',
);
expect($validation['valid'])->toBeFalse()
->and($validation['resolvedClassificationId'])->toBe('discovery-heavy')
->and(implode(' ', $validation['reasons']))->toContain('policy-resource-admin-search-parity');
expect(TestLaneManifest::describeFilePlacement('tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php'))
->toContain('baseline-compare-matrix-workflow', 'ui-workflow', 'Mixed-file');
});
it('publishes heavy-governance author guidance alongside the canonical hotspot family ownership', function (): void {
$inventoryFamilies = collect(TestLaneManifest::heavyGovernanceHotspotInventory())->pluck('familyId')->all();
$guidanceRuleIds = collect(TestLaneManifest::heavyGovernanceAuthorGuidance())->pluck('ruleId')->all();
foreach ($inventoryFamilies as $familyId) {
expect(TestLaneManifest::family($familyId)['targetLaneId'])->toBe('heavy-governance')
->and(TestLaneManifest::family($familyId)['currentLaneId'])->toBeIn(['confidence', 'heavy-governance']);
}
expect($guidanceRuleIds)->toEqualCanonicalizing([
'heavy-family-reuse-before-creation',
'heavy-family-create-only-for-new-trust',
'split-discovery-workflow-surface-concerns',
'retain-intentional-heavy-depth-explicitly',
'record-helper-or-fixture-residuals',
]);
}); });

View File

@ -78,36 +78,46 @@
pest()->group('browser') pest()->group('browser')
->in('Browser'); ->in('Browser');
pest()->group('fast-feedback') pest()->group('ui-light')
->in( ->in(
'Unit', 'Feature/Filament/BackupSetAdminTenantParityTest.php',
'Feature/Auth',
'Feature/Authorization',
'Feature/EntraAdminRoles',
'Feature/Findings',
'Feature/Guards',
'Feature/Monitoring',
'Feature/Navigation',
'Feature/Onboarding',
'Feature/RequiredPermissions',
'Feature/Tenants',
'Feature/Workspaces',
'Feature/AdminConsentCallbackTest.php',
'Feature/AdminNewRedirectTest.php',
); );
pest()->group('heavy-governance') pest()->group('ui-workflow')
->in(
'Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php',
'Feature/Baselines/BaselineCompareMatrixBuilderTest.php',
'Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php',
'Feature/Findings/FindingExceptionRenewalTest.php',
'Feature/Findings/FindingsListFiltersTest.php',
'Feature/Findings/FindingWorkflowRowActionsTest.php',
'Feature/Findings/FindingWorkflowViewActionsTest.php',
'Feature/Filament/BaselineActionAuthorizationTest.php',
'Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'Feature/Findings/FindingBulkActionsTest.php',
'Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'Feature/Rbac/OnboardingWizardUiEnforcementTest.php',
);
pest()->group('surface-guard')
->in( ->in(
'Architecture',
'Deprecation',
'Feature/078',
'Feature/090',
'Feature/144',
'Feature/OpsUx', 'Feature/OpsUx',
'Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'Feature/Filament/Alerts/AlertsKpiHeaderTest.php',
'Feature/Filament/PanelNavigationSegregationTest.php',
'Feature/Filament/TenantReviewHeaderDisciplineTest.php',
'Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php',
'Feature/Guards/ActionSurfaceContractTest.php', 'Feature/Guards/ActionSurfaceContractTest.php',
'Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'Feature/Guards/OperationLifecycleOpsUxGuardTest.php',
'Feature/ProviderConnections/CredentialLeakGuardTest.php', 'Feature/ProviderConnections/CredentialLeakGuardTest.php',
'Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php',
'Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php',
);
pest()->group('discovery-heavy')
->in(
'Feature/Filament/PolicyResourceAdminSearchParityTest.php',
'Feature/Filament/PolicyVersionAdminSearchParityTest.php',
); );
beforeEach(function () { beforeEach(function () {

View File

@ -47,7 +47,9 @@ public function evaluate(float $measuredSeconds): array
$budgetStatus = 'within-budget'; $budgetStatus = 'within-budget';
if ($measuredSeconds > $this->thresholdSeconds) { if ($measuredSeconds > $this->thresholdSeconds) {
$budgetStatus = $this->enforcement === 'warn' ? 'warning' : 'over-budget'; $budgetStatus = in_array($this->enforcement, ['report-only', 'warn'], true)
? 'warning'
: 'over-budget';
} }
return array_filter([ return array_filter([
@ -58,59 +60,311 @@ public function evaluate(float $measuredSeconds): array
'baselineDeltaTargetPercent' => $this->baselineDeltaTargetPercent, 'baselineDeltaTargetPercent' => $this->baselineDeltaTargetPercent,
'measuredSeconds' => round($measuredSeconds, 6), 'measuredSeconds' => round($measuredSeconds, 6),
'budgetStatus' => $budgetStatus, 'budgetStatus' => $budgetStatus,
'notes' => $this->notes,
'reviewCadence' => $this->reviewCadence,
], static fn (mixed $value): bool => $value !== null); ], static fn (mixed $value): bool => $value !== null);
} }
/** /**
* @param list<array<string, mixed>> $familyBudgets * @param array<string, mixed> $budgetTarget
* @param array<string, float|int> $durationsByFile * @return array<string, int|float|string>
* @return list<array<string, int|float|string|array<int, string>>>
*/ */
public static function evaluateFamilyBudgets(array $familyBudgets, array $durationsByFile): array public static function evaluateBudgetTarget(array $budgetTarget, float $measuredSeconds): array
{ {
if (! isset($budgetTarget['targetType'], $budgetTarget['targetId'])) {
throw new InvalidArgumentException('Budget targets must define targetType and targetId.');
}
$evaluation = self::fromArray($budgetTarget)->evaluate($measuredSeconds);
return array_merge([
'budgetId' => (string) ($budgetTarget['budgetId'] ?? sprintf('%s-%s', $budgetTarget['targetType'], $budgetTarget['targetId'])),
'targetType' => (string) $budgetTarget['targetType'],
'targetId' => (string) $budgetTarget['targetId'],
], $evaluation);
}
/**
* @param list<array<string, mixed>> $budgetTargets
* @param array<string, float> $classificationTotals
* @param array<string, float> $familyTotals
* @return list<array<string, int|float|string>>
*/
public static function evaluateBudgetTargets(
array $budgetTargets,
float $laneSeconds,
array $classificationTotals,
array $familyTotals,
): array {
$evaluations = []; $evaluations = [];
foreach ($familyBudgets as $familyBudget) { foreach ($budgetTargets as $budgetTarget) {
$matchedSelectors = []; $targetType = (string) ($budgetTarget['targetType'] ?? '');
$measuredSeconds = 0.0; $targetId = (string) ($budgetTarget['targetId'] ?? '');
$selectorType = (string) ($familyBudget['selectorType'] ?? 'path');
$selectors = array_values(array_filter(
$familyBudget['selectors'] ?? [],
static fn (mixed $selector): bool => is_string($selector) && $selector !== '',
));
foreach ($durationsByFile as $filePath => $duration) { if ($targetType === '' || $targetId === '') {
foreach ($selectors as $selector) { continue;
$matches = match ($selectorType) {
'file' => $filePath === $selector,
default => str_starts_with($filePath, rtrim($selector, '/')),
};
if (! $matches) {
continue;
}
$matchedSelectors[] = $selector;
$measuredSeconds += (float) $duration;
break;
}
} }
$budget = self::fromArray([ $measuredSeconds = match ($targetType) {
'thresholdSeconds' => (int) $familyBudget['thresholdSeconds'], 'lane' => $laneSeconds,
'baselineSource' => (string) $familyBudget['baselineSource'], 'classification' => (float) ($classificationTotals[$targetId] ?? 0.0),
'enforcement' => (string) $familyBudget['enforcement'], 'family' => (float) ($familyTotals[$targetId] ?? 0.0),
'lifecycleState' => (string) $familyBudget['lifecycleState'], default => 0.0,
]); };
$evaluations[] = array_merge([ $evaluations[] = self::evaluateBudgetTarget($budgetTarget, $measuredSeconds);
'familyId' => (string) $familyBudget['familyId'],
], $budget->evaluate($measuredSeconds), [
'matchedSelectors' => array_values(array_unique($matchedSelectors)),
]);
} }
return $evaluations; return $evaluations;
} }
/**
* @param array<string, mixed> $contract
* @return array<string, int|float|string>
*/
public static function evaluateGovernanceContract(array $contract, float $measuredSeconds): array
{
foreach (['laneId', 'summaryThresholdSeconds', 'evaluationThresholdSeconds', 'normalizedThresholdSeconds'] as $requiredKey) {
if (! array_key_exists($requiredKey, $contract)) {
throw new InvalidArgumentException(sprintf('Governance contracts must define [%s].', $requiredKey));
}
}
$normalizedThresholdSeconds = (float) $contract['normalizedThresholdSeconds'];
if ($normalizedThresholdSeconds <= 0) {
throw new InvalidArgumentException('Governance contracts must define a positive normalizedThresholdSeconds value.');
}
$enforcementLevel = (string) ($contract['enforcementLevel'] ?? 'warn');
$budgetStatus = 'within-budget';
if ($measuredSeconds > $normalizedThresholdSeconds) {
$budgetStatus = in_array($enforcementLevel, ['report-only', 'warn'], true)
? 'warning'
: 'over-budget';
}
return array_filter([
'laneId' => (string) $contract['laneId'],
'summaryThresholdSeconds' => (float) $contract['summaryThresholdSeconds'],
'evaluationThresholdSeconds' => (float) $contract['evaluationThresholdSeconds'],
'normalizedThresholdSeconds' => $normalizedThresholdSeconds,
'baselineSource' => (string) ($contract['baselineSource'] ?? 'measured-lane'),
'enforcementLevel' => $enforcementLevel,
'lifecycleState' => (string) ($contract['lifecycleState'] ?? 'draft'),
'decisionStatus' => (string) ($contract['decisionStatus'] ?? 'pending'),
'measuredSeconds' => round($measuredSeconds, 6),
'budgetStatus' => $budgetStatus,
'reconciliationRationale' => isset($contract['reconciliationRationale']) ? (string) $contract['reconciliationRationale'] : null,
], static fn (mixed $value): bool => $value !== null);
}
/**
* @return list<array<string, int|float|string>>
*/
public static function enforcementProfiles(): array
{
$fastFeedbackBudget = TestLaneManifest::lane('fast-feedback')['budget'];
$confidenceBudget = TestLaneManifest::lane('confidence')['budget'];
$browserBudget = TestLaneManifest::lane('browser')['budget'];
$heavyGovernanceContract = TestLaneManifest::heavyGovernanceBudgetContract();
return [
[
'policyId' => 'pr-fast-feedback-budget',
'laneId' => 'fast-feedback',
'triggerClass' => 'pull-request',
'thresholdSource' => 'lane-budget',
'baseThresholdSeconds' => (int) $fastFeedbackBudget['thresholdSeconds'],
'varianceAllowanceSeconds' => 15,
'effectiveThresholdSeconds' => (int) $fastFeedbackBudget['thresholdSeconds'] + 15,
'enforcementMode' => 'hard-fail',
'lifecycleState' => (string) $fastFeedbackBudget['lifecycleState'],
'reviewCadence' => 'revisit after two stable CI pull request runs',
],
[
'policyId' => 'main-confidence-budget',
'laneId' => 'confidence',
'triggerClass' => 'mainline-push',
'thresholdSource' => 'lane-budget',
'baseThresholdSeconds' => (int) $confidenceBudget['thresholdSeconds'],
'varianceAllowanceSeconds' => 30,
'effectiveThresholdSeconds' => (int) $confidenceBudget['thresholdSeconds'] + 30,
'enforcementMode' => 'soft-warn',
'lifecycleState' => (string) $confidenceBudget['lifecycleState'],
'reviewCadence' => 'tighten after two stable dev runs',
],
[
'policyId' => 'heavy-governance-manual-budget',
'laneId' => 'heavy-governance',
'triggerClass' => 'manual',
'thresholdSource' => 'governance-contract',
'baseThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']),
'varianceAllowanceSeconds' => 15,
'effectiveThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']) + 15,
'enforcementMode' => 'soft-warn',
'lifecycleState' => (string) $heavyGovernanceContract['lifecycleState'],
'reviewCadence' => 'manual heavy validation must stabilize before schedule enablement',
],
[
'policyId' => 'heavy-governance-scheduled-budget',
'laneId' => 'heavy-governance',
'triggerClass' => 'scheduled',
'thresholdSource' => 'governance-contract',
'baseThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']),
'varianceAllowanceSeconds' => 15,
'effectiveThresholdSeconds' => (int) round((float) $heavyGovernanceContract['normalizedThresholdSeconds']) + 15,
'enforcementMode' => 'trend-only',
'lifecycleState' => (string) $heavyGovernanceContract['lifecycleState'],
'reviewCadence' => 'convert from trend-only only after stable scheduled evidence exists',
],
[
'policyId' => 'browser-manual-budget',
'laneId' => 'browser',
'triggerClass' => 'manual',
'thresholdSource' => 'lane-budget',
'baseThresholdSeconds' => (int) $browserBudget['thresholdSeconds'],
'varianceAllowanceSeconds' => 20,
'effectiveThresholdSeconds' => (int) $browserBudget['thresholdSeconds'] + 20,
'enforcementMode' => 'soft-warn',
'lifecycleState' => (string) $browserBudget['lifecycleState'],
'reviewCadence' => 'tighten after two stable browser validation runs',
],
[
'policyId' => 'browser-scheduled-budget',
'laneId' => 'browser',
'triggerClass' => 'scheduled',
'thresholdSource' => 'lane-budget',
'baseThresholdSeconds' => (int) $browserBudget['thresholdSeconds'],
'varianceAllowanceSeconds' => 20,
'effectiveThresholdSeconds' => (int) $browserBudget['thresholdSeconds'] + 20,
'enforcementMode' => 'trend-only',
'lifecycleState' => (string) $browserBudget['lifecycleState'],
'reviewCadence' => 'convert from trend-only only after stable scheduled evidence exists',
],
];
}
/**
* @return array<string, int|float|string>
*/
public static function enforcementProfile(string $laneId, string $triggerClass): array
{
foreach (self::enforcementProfiles() as $profile) {
if ($profile['laneId'] === $laneId && $profile['triggerClass'] === $triggerClass) {
return $profile;
}
}
throw new InvalidArgumentException(sprintf('Unknown trigger-aware budget profile for lane [%s] and trigger [%s].', $laneId, $triggerClass));
}
/**
* @param array<string, int|float|string> $profile
* @return array<string, int|float|string|null>
*/
public static function evaluateTriggerAwareBudgetProfile(array $profile, float $measuredSeconds): array
{
foreach (['laneId', 'triggerClass', 'baseThresholdSeconds', 'varianceAllowanceSeconds', 'effectiveThresholdSeconds', 'enforcementMode', 'lifecycleState'] as $requiredKey) {
if (! array_key_exists($requiredKey, $profile)) {
throw new InvalidArgumentException(sprintf('Trigger-aware budget profiles must define [%s].', $requiredKey));
}
}
$baseThresholdSeconds = (float) $profile['baseThresholdSeconds'];
$effectiveThresholdSeconds = (float) $profile['effectiveThresholdSeconds'];
$budgetStatus = 'within-budget';
if ($measuredSeconds > $effectiveThresholdSeconds) {
$budgetStatus = 'over-budget';
} elseif ($measuredSeconds > $baseThresholdSeconds) {
$budgetStatus = 'warning';
}
$blockingStatus = match ((string) $profile['enforcementMode']) {
'hard-fail' => match ($budgetStatus) {
'over-budget' => 'blocking',
'warning' => 'non-blocking-warning',
default => 'informational',
},
'soft-warn' => $budgetStatus === 'within-budget' ? 'informational' : 'non-blocking-warning',
'trend-only' => 'informational',
default => throw new InvalidArgumentException(sprintf('Unknown enforcement mode [%s].', $profile['enforcementMode'])),
};
return array_merge($profile, [
'measuredSeconds' => round($measuredSeconds, 6),
'budgetStatus' => $budgetStatus,
'blockingStatus' => $blockingStatus,
'primaryFailureClassId' => $budgetStatus === 'within-budget' ? null : 'budget-breach',
]);
}
/**
* @return array<string, int|float|string|null>
*/
public static function evaluateLaneForTrigger(string $laneId, string $triggerClass, float $measuredSeconds): array
{
return self::evaluateTriggerAwareBudgetProfile(
self::enforcementProfile($laneId, $triggerClass),
$measuredSeconds,
);
}
/**
* @param array<string, mixed> $baselineSnapshot
* @param array<string, mixed> $currentSnapshot
* @return array<string, float>
*/
public static function compareSnapshots(array $baselineSnapshot, array $currentSnapshot): array
{
$baselineSeconds = round((float) ($baselineSnapshot['wallClockSeconds'] ?? 0.0), 6);
$currentSeconds = round((float) ($currentSnapshot['wallClockSeconds'] ?? 0.0), 6);
$deltaSeconds = round($currentSeconds - $baselineSeconds, 6);
$deltaPercent = $baselineSeconds > 0.0
? round(($deltaSeconds / $baselineSeconds) * 100, 6)
: 0.0;
return [
'baselineSeconds' => $baselineSeconds,
'currentSeconds' => $currentSeconds,
'deltaSeconds' => $deltaSeconds,
'deltaPercent' => $deltaPercent,
];
}
/**
* @param array<string, mixed> $contract
* @param array<string, mixed> $baselineSnapshot
* @param array<string, mixed> $currentSnapshot
* @param list<string> $remainingOpenFamilies
* @param list<string> $followUpDebt
* @return array<string, mixed>
*/
public static function buildOutcomeRecord(
array $contract,
array $baselineSnapshot,
array $currentSnapshot,
array $remainingOpenFamilies,
string $justification,
array $followUpDebt = [],
): array {
$comparison = self::compareSnapshots($baselineSnapshot, $currentSnapshot);
return array_filter([
'outcomeId' => sprintf('%s-final-outcome', (string) ($contract['laneId'] ?? 'heavy-governance')),
'decisionStatus' => (string) ($contract['decisionStatus'] ?? 'pending'),
'finalThresholdSeconds' => round((float) ($contract['normalizedThresholdSeconds'] ?? 0.0), 6),
'finalMeasuredSeconds' => $comparison['currentSeconds'],
'deltaSeconds' => $comparison['deltaSeconds'],
'deltaPercent' => $comparison['deltaPercent'],
'remainingOpenFamilies' => array_values($remainingOpenFamilies),
'justification' => $justification,
'followUpDebt' => $followUpDebt !== [] ? array_values($followUpDebt) : null,
], static fn (mixed $value): bool => $value !== null);
}
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -25,25 +25,33 @@
->and($hardFailBudget->evaluate(31.2)['budgetStatus'])->toBe('over-budget'); ->and($hardFailBudget->evaluate(31.2)['budgetStatus'])->toBe('over-budget');
}); });
it('evaluates initial family thresholds from matched file durations', function (): void { it('evaluates family targets through the generic budget target path', function (): void {
$evaluations = TestLaneBudget::evaluateFamilyBudgets( $familyTargets = array_values(array_filter(
TestLaneManifest::familyBudgets(), TestLaneManifest::budgetTargets(),
static fn (array $target): bool => $target['targetType'] === 'family',
));
$evaluations = TestLaneBudget::evaluateBudgetTargets(
$familyTargets,
0.0,
[],
[ [
'tests/Feature/OpsUx/OperateHubShellTest.php' => 18.4, 'ops-ux-governance' => 18.4,
'tests/Feature/Guards/ActionSurfaceContractTest.php' => 7.8, 'action-surface-contract' => 7.8,
'tests/Browser/Spec198MonitoringPageStateSmokeTest.php' => 14.2, 'browser-smoke' => 14.2,
], ],
); );
expect($evaluations)->not->toBeEmpty() expect($evaluations)->not->toBeEmpty()
->and($evaluations[0])->toHaveKeys([ ->and($evaluations[0])->toHaveKeys([
'familyId', 'budgetId',
'targetType',
'targetId',
'thresholdSeconds', 'thresholdSeconds',
'baselineSource', 'baselineSource',
'enforcement', 'enforcement',
'lifecycleState', 'lifecycleState',
'measuredSeconds', 'measuredSeconds',
'budgetStatus', 'budgetStatus',
'matchedSelectors',
]); ]);
}); });

View File

@ -3,7 +3,7 @@ # Product Roadmap
> Strategic thematic blocks and release trajectory. > Strategic thematic blocks and release trajectory.
> This is the "big picture" — not individual specs. > This is the "big picture" — not individual specs.
**Last updated**: 2026-04-12 **Last updated**: 2026-04-17
--- ---
@ -76,6 +76,15 @@ ### R2 Completion — Evidence & Exception Workflows
- Formal "evidence pack" entity → **Not yet specced** - Formal "evidence pack" entity → **Not yet specced**
- Workspace-level PII override for review packs → deferred from 109 - Workspace-level PII override for review packs → deferred from 109
### Findings Workflow v2 / Execution Layer
Turn findings from a reviewable register into an accountable operating flow with clear ownership, personal queues, intake, hygiene, and minimal escalation.
**Scope direction**:
- Clarify owner versus assignee semantics and accountability language first
- Add operator work surfaces such as "Assigned to me" and an unassigned intake queue with basic claim flow
- Harden assignment hygiene, stale-work detection, and resolved-versus-verified outcome semantics
- Reuse the existing alerting foundation for assignment, reopen, due-soon, and overdue notification flows
- Keep comments, external ticket handoff, and cross-tenant workboards as later slices instead of forcing them into the first workflow iteration
### Policy Lifecycle / Ghost Policies ### Policy Lifecycle / Ghost Policies
Soft delete detection, automatic restore, "Deleted" badge, restore from backup. Soft delete detection, automatic restore, "Deleted" badge, restore from backup.
Draft exists (Spec 900). Needs spec refresh and prioritization. Draft exists (Spec 900). Needs spec refresh and prioritization.
@ -101,6 +110,7 @@ ### MSP Portfolio & Operations (Multi-Tenant)
Multi-tenant health dashboard, SLA/compliance reports (PDF), cross-tenant troubleshooting center. Multi-tenant health dashboard, SLA/compliance reports (PDF), cross-tenant troubleshooting center.
**Source**: 0800-future-features brainstorming, identified as highest priority pillar. **Source**: 0800-future-features brainstorming, identified as highest priority pillar.
**Prerequisites**: Decision-Based Operating Foundations, Cross-tenant compare (Spec 043 — draft only). **Prerequisites**: Decision-Based Operating Foundations, Cross-tenant compare (Spec 043 — draft only).
**Later expansion**: portfolio operations should eventually include a cross-tenant findings workboard once tenant-level inbox and intake flows are stable.
### Human-in-the-Loop Autonomous Governance (Decision-Based Operating) ### Human-in-the-Loop Autonomous Governance (Decision-Based Operating)
Continuous detection, triage, decision drafting, approval-driven execution, and closed-loop evidence for governance actions across the workspace portfolio. Continuous detection, triage, decision drafting, approval-driven execution, and closed-loop evidence for governance actions across the workspace portfolio.
@ -118,6 +128,10 @@ ### Standardization & Policy Quality ("Intune Linting")
Policy linter (naming, scope tag requirements, no All-Users on high-risk), company standards as templates, policy hygiene (duplicate finder, unassigned, orphaned, stale). Policy linter (naming, scope tag requirements, no All-Users on high-risk), company standards as templates, policy hygiene (duplicate finder, unassigned, orphaned, stale).
**Source**: 0800-future-features brainstorming. **Source**: 0800-future-features brainstorming.
### PSA / Ticketing Handoff
Outbound handoff from findings into external service-desk or PSA systems with visible `ticket_ref` linkage and auditable "ticket created/linked" events.
**Scope direction**: start with one-way handoff and internal visibility, not full bidirectional sync or full ITSM modeling.
### Compliance Readiness & Executive Review Packs ### Compliance Readiness & Executive Review Packs
On-demand review packs that combine governance findings, accepted risks, evidence, baseline/drift posture, and key security signals into one coherent deliverable. BSI-/NIS2-/CIS-oriented readiness views (without certification claims). Executive / CISO / customer-facing report surfaces alongside operator-facing detail views. Exportable auditor-ready and management-ready outputs. On-demand review packs that combine governance findings, accepted risks, evidence, baseline/drift posture, and key security signals into one coherent deliverable. BSI-/NIS2-/CIS-oriented readiness views (without certification claims). Executive / CISO / customer-facing report surfaces alongside operator-facing detail views. Exportable auditor-ready and management-ready outputs.
**Goal**: Make TenantPilot sellable as an MSP-facing governance and review platform for German midmarket and compliance-oriented customers who want structured tenant reviews and management-ready outputs on demand. **Goal**: Make TenantPilot sellable as an MSP-facing governance and review platform for German midmarket and compliance-oriented customers who want structured tenant reviews and management-ready outputs on demand.

View File

@ -5,7 +5,7 @@ # Spec Candidates
> >
> **Flow**: Inbox → Qualified → Planned → Spec created → moved to `Promoted to Spec` > **Flow**: Inbox → Qualified → Planned → Spec created → moved to `Promoted to Spec`
**Last reviewed**: 2026-04-12 (added decision-based operating foundations and autonomous governance track) **Last reviewed**: 2026-04-17 (added findings execution layer cluster)
--- ---
@ -579,6 +579,113 @@ ### Exception / Risk-Acceptance Workflow for Findings
- **Dependencies**: Findings workflow (Spec 111) complete, audit log foundation (Spec 134) - **Dependencies**: Findings workflow (Spec 111) complete, audit log foundation (Spec 134)
- **Priority**: high - **Priority**: high
> Findings execution layer cluster: complementary to the existing risk-acceptance candidate. Keep these split so prioritization can pull workflow semantics, operator work surfaces, alerts, external handoff, and later portfolio operating slices independently instead of collapsing them into one oversized "Findings v2" spec.
### Finding Ownership Semantics Clarification
- **Type**: domain semantics / workflow hardening
- **Source**: findings execution layer candidate pack 2026-04-17; current Finding owner/assignee overlap analysis
- **Problem**: Finding already models `owner` and `assignee`, but the semantic split is not crisp enough to support inboxes, escalation, stale-work detection, or consistent audit language. Accountability and active execution responsibility can currently blur together in UI copy, policies, and workflow rules.
- **Why it matters**: Without a shared contract, every downstream workflow slice will invent its own meaning for owner versus assignee. That produces ambiguous queues, brittle escalation rules, and inconsistent governance language.
- **Proposed direction**: Define canonical semantics for accountability owner versus active assignee; align labels, policies, audit/event vocabulary, and permission expectations around that split; encode expected lifecycle states for unassigned, assigned, reassigned, and orphaned work without introducing team-queue abstractions yet.
- **Explicit non-goals**: Team-/queue-based ownership, ticketing, comments, and notification delivery.
- **Dependencies**: Existing Findings model and workflow state machine, Findings UI surfaces, audit vocabulary.
- **Roadmap fit**: Findings Workflow v2 hardening lane.
- **Strategic sequencing**: First. The rest of the findings execution layer consumes this decision.
- **Priority**: high
### Findings Operator Inbox v1
- **Type**: operator surface / workflow execution
- **Source**: findings execution layer candidate pack 2026-04-17; gap between assignment auditability and day-to-day operator flow
- **Problem**: Findings can be assigned, but TenantPilot lacks a personal work surface that turns assignment into a real operator queue. Assigned work is still discovered indirectly through broader tenant or findings lists.
- **Why it matters**: Without a dedicated personal queue, assignment remains metadata instead of operational flow. A "My Work" surface is the simplest bridge from governance data to daily execution.
- **Proposed direction**: Add a workspace-level or otherwise permission-safe "My Findings / My Work" surface for the current user; emphasize open, due, overdue, high-severity, and reopened findings; provide fast drilldown into the finding record; add a small "assigned to me" dashboard signal; reuse existing RBAC and finding visibility rules instead of inventing a second permission system.
- **Explicit non-goals**: Team queues, complex routing rules, external ticketing, and multi-step approval chains.
- **Dependencies**: Ownership semantics, `assignee_user_id`, `due_at`, finding status logic, RBAC on finding read/open.
- **Roadmap fit**: Findings Workflow v2; feeds later governance inbox work.
- **Strategic sequencing**: Second, ideally immediately after ownership semantics.
- **Priority**: high
### Findings Intake & Team Queue v1
- **Type**: workflow execution / team operations
- **Source**: findings execution layer candidate pack 2026-04-17; missing intake surface before personal assignment
- **Problem**: A personal inbox does not solve how new or unassigned findings enter the workflow. Operators need an intake surface before work is personally assigned.
- **Why it matters**: Without intake, backlog triage stays hidden in general-purpose lists and unassigned work becomes easy to ignore or duplicate.
- **Proposed direction**: Introduce unassigned and needs-triage views, an optional claim action, and basic shared-worklist conventions; use filters or tabs that clearly separate intake from active execution; make the difference between unowned backlog and personally assigned work explicit.
- **Explicit non-goals**: Full team model, capacity planning, auto-routing, and load-balancing logic.
- **Dependencies**: Ownership semantics, findings filters/tabs, open-status definitions.
- **Roadmap fit**: Findings Workflow v2; prerequisite for a broader team operating model.
- **Strategic sequencing**: Third, after personal inbox foundations exist.
- **Priority**: high
### Findings Notifications & Escalation v1
- **Type**: alerts / workflow execution
- **Source**: findings execution layer candidate pack 2026-04-17; gap between assignment metadata and actionable control loop
- **Problem**: Assignment, reopen, due, and overdue states currently risk becoming silent metadata unless operators keep polling findings views.
- **Why it matters**: Due dates without reminders or escalation are visibility, not control. Existing alert foundations only create operator value if findings workflow emits actionable events.
- **Proposed direction**: Add notifications for assignment, system-driven reopen, due-soon, and overdue states; introduce minimal escalation to owner or a defined role; explicitly consume the existing alert and notification infrastructure rather than building a findings-specific delivery system.
- **Explicit non-goals**: Multi-stage escalation chains, a large notification-preference center, and bidirectional ticket synchronization.
- **Dependencies**: Ownership semantics, operator inbox/intake surfaces, due/SLA logic, alert plumbing.
- **Roadmap fit**: Findings workflow hardening on top of the existing alerting foundation.
- **Strategic sequencing**: After inbox and intake exist so notifications land on meaningful destinations.
- **Priority**: high
### Assignment Hygiene & Stale Work Detection
- **Type**: workflow hardening / operations hygiene
- **Source**: findings execution layer candidate pack 2026-04-17; assignment lifecycle hygiene gap analysis
- **Problem**: Assignments can silently rot when memberships change, assignees lose access, or findings remain stuck in `in_progress` indefinitely.
- **Why it matters**: Stale and orphaned assignments erode trust in queues and create hidden backlog. Hygiene reporting is a prerequisite for later auto-reassign logic.
- **Proposed direction**: Detect orphaned assignments, invalid or inactive assignees, and stale `in_progress` work; provide an admin/operator hygiene report; define what counts as stale versus active; stop short of automatic redistribution in v1.
- **Explicit non-goals**: Full reassignment workflows, automatic load distribution, and absence management.
- **Dependencies**: Tenant membership / RBAC model, scheduler or job layer, ownership semantics, open status logic.
- **Roadmap fit**: Findings Workflow v2 hardening.
- **Strategic sequencing**: Shortly after ownership semantics, ideally alongside or immediately after notifications.
- **Priority**: high
### Finding Outcome Taxonomy & Verification Semantics
- **Type**: workflow semantics / reporting hardening
- **Source**: findings execution layer candidate pack 2026-04-17; status/outcome reporting gap analysis
- **Problem**: Resolve and close reasoning is too free-form, and the product does not cleanly separate operator-resolved from system-verified or confirmed-cleared states.
- **Why it matters**: Reporting, audit, reopening logic, and governance review packs need structured outcomes rather than ad hoc prose. Otherwise outcome meaning drifts between operators and surfaces.
- **Proposed direction**: Define structured reason codes for resolve, close, and reopen transitions; distinguish resolved, verified or confirmed cleared, closed, false positive, duplicate, and no-longer-applicable semantics; make reporting and filter UI consume the taxonomy instead of relying on free text.
- **Explicit non-goals**: Comments, full narrative case notes, and complex multi-reason models.
- **Dependencies**: Finding status transitions, audit payloads, reporting and filter UI.
- **Roadmap fit**: Findings Workflow v2 hardening and downstream review/reporting quality.
- **Strategic sequencing**: After the first operator work surfaces unless reporting pressure pulls it earlier.
- **Priority**: high
### Finding Comments & Decision Log v1
- **Type**: collaboration / audit depth
- **Source**: findings execution layer candidate pack 2026-04-17; operator handoff and context gap analysis
- **Problem**: Audit can show that a transition happened, but not the day-to-day operator reasoning or handover context behind triage, resolve, close, or risk-accept decisions.
- **Why it matters**: Human workflow needs concise contextual notes that do not belong in status fields or reason-code taxonomies. Without them, operator handover quality stays low.
- **Proposed direction**: Add comments and lightweight decision-log entries on findings; surface them in a timeline alongside immutable audit events; use them to support triage, handover, and rationale capture without turning findings into a chat product.
- **Explicit non-goals**: `@mentions`, attachments, chat, and realtime collaboration.
- **Dependencies**: Finding detail surface, audit/timeline rendering, RBAC.
- **Roadmap fit**: Spec-candidate only for now; not required as a standalone roadmap theme.
- **Priority**: medium
### Findings External Ticket Handoff v1
- **Type**: external integration / execution handoff
- **Source**: findings execution layer candidate pack 2026-04-17; MSP/enterprise workflow alignment gap
- **Problem**: Enterprise and MSP operators often need to hand findings into external service-desk or PSA workflows, but the current findings model has no first-class outbound ticket link or handoff state.
- **Why it matters**: Outbound handoff is a sellable bridge between TenantPilot governance and existing customer operating models. Without it, findings remain operationally isolated from the systems where remediation work actually gets tracked.
- **Proposed direction**: Add an external ticket reference and link on findings, support simple outbound handoff or "ticket created/linked" flows, and audit those transitions explicitly; make internal versus external execution state visible without promising full synchronization.
- **Explicit non-goals**: Bidirectional sync, OAuth-native integrations, and full ITSM domain modeling.
- **Dependencies**: Findings UI, workspace settings or handoff target configuration, audit events, stable ownership semantics.
- **Roadmap fit**: Future PSA/ticketing lane.
- **Priority**: medium
### Cross-Tenant Findings Workboard v1
- **Type**: MSP / portfolio operations
- **Source**: findings execution layer candidate pack 2026-04-17; portfolio-scale findings operations gap
- **Problem**: Once operators manage many tenants, tenant-local inboxes and queues stop scaling. There is no cross-tenant work surface for open findings across a workspace or portfolio.
- **Why it matters**: MSP portfolio work requires cross-tenant prioritization by severity, due date, assignee, and tenant. This is the operational complement to a portfolio dashboard.
- **Proposed direction**: Add a cross-tenant findings workboard or queue for open findings with filters for severity, due date, assignee, tenant, and status; preserve tenant drilldown and RBAC boundaries; position it as the portfolio-operating surface next to the dashboard, not a replacement for per-tenant detail.
- **Explicit non-goals**: Rollout orchestration, full portfolio remediation planning, and team capacity views.
- **Dependencies**: Operator inbox, intake queue, notifications/escalation, workspace-level finding visibility, cross-tenant RBAC semantics.
- **Roadmap fit**: MSP portfolio and operations.
- **Priority**: medium-low
### Compliance Control Catalog & Interpretation Foundation ### Compliance Control Catalog & Interpretation Foundation
- **Type**: foundation - **Type**: foundation
- **Source**: roadmap/principles alignment 2026-04-10, compliance modeling discussion, future framework-oriented readiness planning - **Source**: roadmap/principles alignment 2026-04-10, compliance modeling discussion, future framework-oriented readiness planning

73
scripts/platform-test-artifacts Executable file
View File

@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/platform"
LANE="${1:-}"
STAGING_DIRECTORY="${2:-}"
ARTIFACT_DIRECTORY=""
WORKFLOW_ID="${TENANTATLAS_CI_WORKFLOW_ID:-}"
TRIGGER_CLASS="${TENANTATLAS_CI_TRIGGER_CLASS:-}"
if [[ -z "${LANE}" || -z "${STAGING_DIRECTORY}" ]]; then
echo "Usage: ./scripts/platform-test-artifacts <lane-id> <staging-directory> [--artifact-directory=storage/logs/test-lanes] [--workflow-id=<id>] [--trigger-class=<trigger>]" >&2
exit 1
fi
shift 2 || true
for arg in "$@"; do
if [[ "${arg}" == --artifact-directory=* ]]; then
ARTIFACT_DIRECTORY="${arg#--artifact-directory=}"
continue
fi
if [[ "${arg}" == --workflow-id=* ]]; then
WORKFLOW_ID="${arg#--workflow-id=}"
continue
fi
if [[ "${arg}" == --trigger-class=* ]]; then
TRIGGER_CLASS="${arg#--trigger-class=}"
continue
fi
echo "Unknown option: ${arg}" >&2
exit 1
done
if [[ -n "${WORKFLOW_ID}" ]]; then
export TENANTATLAS_CI_WORKFLOW_ID="${WORKFLOW_ID}"
fi
if [[ -n "${TRIGGER_CLASS}" ]]; then
export TENANTATLAS_CI_TRIGGER_CLASS="${TRIGGER_CLASS}"
fi
cd "${APP_DIR}"
LANE_ID="${LANE}" \
STAGING_DIRECTORY="${STAGING_DIRECTORY}" \
ARTIFACT_DIRECTORY="${ARTIFACT_DIRECTORY}" \
./vendor/bin/sail php <<'PHP'
<?php
declare(strict_types=1);
require 'vendor/autoload.php';
$laneId = (string) getenv('LANE_ID');
$stagingDirectory = (string) getenv('STAGING_DIRECTORY');
$artifactDirectory = getenv('ARTIFACT_DIRECTORY');
$artifactDirectory = is_string($artifactDirectory) && trim($artifactDirectory) !== ''
? $artifactDirectory
: null;
$result = \Tests\Support\TestLaneReport::stageArtifacts($laneId, $stagingDirectory, $artifactDirectory);
echo json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
exit(($result['complete'] ?? false) === true ? 0 : 1);
PHP

View File

@ -6,6 +6,23 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/platform" APP_DIR="${ROOT_DIR}/apps/platform"
LANE="${1:-fast-feedback}" LANE="${1:-fast-feedback}"
CAPTURE_BASELINE=false
WORKFLOW_ID=""
TRIGGER_CLASS=""
copy_heavy_baseline_artifacts() {
local artifact_root="${APP_DIR}/storage/logs/test-lanes"
local suffix
for suffix in junit.xml summary.md budget.json report.json profile.txt; do
local latest_path="${artifact_root}/heavy-governance-latest.${suffix}"
local baseline_path="${artifact_root}/heavy-governance-baseline.${suffix}"
if [[ -f "${latest_path}" ]]; then
cp "${latest_path}" "${baseline_path}"
fi
done
}
case "${LANE}" in case "${LANE}" in
fast-feedback|fast|default) fast-feedback|fast|default)
@ -34,6 +51,48 @@ esac
shift || true shift || true
remaining_args=()
for arg in "$@"; do
if [[ "${arg}" == "--capture-baseline" ]]; then
CAPTURE_BASELINE=true
continue
fi
if [[ "${arg}" == --workflow-id=* ]]; then
WORKFLOW_ID="${arg#--workflow-id=}"
continue
fi
if [[ "${arg}" == --trigger-class=* ]]; then
TRIGGER_CLASS="${arg#--trigger-class=}"
continue
fi
remaining_args+=("${arg}")
done
if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LANE}" != "heavy" ]]; then
echo "--capture-baseline is only supported for heavy-governance" >&2
exit 1
fi
if [[ -n "${WORKFLOW_ID}" ]]; then
export TENANTATLAS_CI_WORKFLOW_ID="${WORKFLOW_ID}"
fi
if [[ -n "${TRIGGER_CLASS}" ]]; then
export TENANTATLAS_CI_TRIGGER_CLASS="${TRIGGER_CLASS}"
fi
cd "${APP_DIR}" cd "${APP_DIR}"
exec ./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "$@" if [[ ${#remaining_args[@]} -gt 0 ]]; then
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "${remaining_args[@]}"
else
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
fi
if [[ "${CAPTURE_BASELINE}" == true ]]; then
copy_heavy_baseline_artifacts
fi

View File

@ -6,7 +6,85 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/platform" APP_DIR="${ROOT_DIR}/apps/platform"
LANE="${1:-fast-feedback}" LANE="${1:-fast-feedback}"
CAPTURE_BASELINE=false
WORKFLOW_ID=""
TRIGGER_CLASS=""
copy_heavy_baseline_artifacts() {
local artifact_root="${APP_DIR}/storage/logs/test-lanes"
local suffix
for suffix in summary.md budget.json report.json; do
local latest_path="${artifact_root}/heavy-governance-latest.${suffix}"
local baseline_path="${artifact_root}/heavy-governance-baseline.${suffix}"
if [[ -f "${latest_path}" ]]; then
cp "${latest_path}" "${baseline_path}"
fi
done
}
case "${LANE}" in
fast-feedback|fast|default)
COMPOSER_SCRIPT="test:report"
;;
confidence)
COMPOSER_SCRIPT="test:report:confidence"
;;
browser)
COMPOSER_SCRIPT="test:report:browser"
;;
heavy-governance|heavy)
COMPOSER_SCRIPT="test:report:heavy"
;;
profiling|profile)
COMPOSER_SCRIPT="test:report:profile"
;;
*)
echo "Unknown test lane: ${LANE}" >&2
exit 1
;;
esac
shift || true
for arg in "$@"; do
if [[ "${arg}" == "--capture-baseline" ]]; then
CAPTURE_BASELINE=true
continue
fi
if [[ "${arg}" == --workflow-id=* ]]; then
WORKFLOW_ID="${arg#--workflow-id=}"
continue
fi
if [[ "${arg}" == --trigger-class=* ]]; then
TRIGGER_CLASS="${arg#--trigger-class=}"
continue
fi
echo "Unknown option: ${arg}" >&2
exit 1
done
if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LANE}" != "heavy" ]]; then
echo "--capture-baseline is only supported for heavy-governance" >&2
exit 1
fi
if [[ -n "${WORKFLOW_ID}" ]]; then
export TENANTATLAS_CI_WORKFLOW_ID="${WORKFLOW_ID}"
fi
if [[ -n "${TRIGGER_CLASS}" ]]; then
export TENANTATLAS_CI_TRIGGER_CLASS="${TRIGGER_CLASS}"
fi
cd "${APP_DIR}" cd "${APP_DIR}"
exec ./vendor/bin/sail php -r 'require "vendor/autoload.php"; exit(\Tests\Support\TestLaneManifest::renderLatestReport((string) ($argv[1] ?? ""), (string) ($argv[2] ?? "shared-test-fixture-slimming")));' "${LANE}" "shared-test-fixture-slimming" ./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
if [[ "${CAPTURE_BASELINE}" == true ]]; then
copy_heavy_baseline_artifacts
fi

View File

@ -0,0 +1,36 @@
# Specification Quality Checklist: Filament/Livewire Heavy Suite Segmentation
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-16
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation completed in one pass on 2026-04-16.
- Filament and Livewire are used as scope-defining names for the affected test families, not as implementation prescriptions.
- The specification remains bounded to repository test-governance behavior and leaves CI wiring to the follow-up planning sequence.

View File

@ -0,0 +1,666 @@
openapi: 3.1.0
info:
title: Heavy Suite Segmentation Logical Contract
version: 1.0.0
summary: Logical contract for classifying heavy UI test families, validating lane placement, and reading heavy-attribution reports.
description: |
This is a logical contract for repository tooling, tests, and planning artifacts.
It does not imply a new runtime HTTP service. It documents the expected
semantics of heavy-family classification, lane-placement validation, and
heavy-lane report attribution so the existing lane-governance seams remain
consistent as Spec 208 is implemented.
x-logical-contract: true
servers:
- url: https://tenantatlas.local/logical
paths:
/heavy-test-classifications:
get:
summary: Read the checked-in heavy classification catalog.
operationId: listHeavyTestClassifications
responses:
'200':
description: Current heavy classification catalog.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- classifications
properties:
classifications:
type: array
minItems: 5
items:
$ref: '#/components/schemas/HeavyTestClassification'
/heavy-test-placement-rules:
get:
summary: Read the checked-in lane placement rules.
operationId: listHeavyTestPlacementRules
responses:
'200':
description: Current lane placement rules.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- rules
properties:
rules:
type: array
minItems: 5
items:
$ref: '#/components/schemas/LanePlacementRule'
/heavy-test-families:
get:
summary: Read the seeded heavy test family inventory.
operationId: listHeavyTestFamilies
responses:
'200':
description: Current heavy family inventory.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- families
properties:
families:
type: array
minItems: 1
items:
$ref: '#/components/schemas/HeavyTestFamily'
/heavy-test-families/mixed-file-resolutions:
get:
summary: Read the checked-in mixed-file resolution inventory.
operationId: listMixedFileResolutions
responses:
'200':
description: Current mixed-file resolution records.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- resolutions
properties:
resolutions:
type: array
items:
$ref: '#/components/schemas/MixedFileResolution'
/heavy-test-families/placement-validations:
post:
summary: Validate that a heavy class or family is assigned to a compatible lane.
operationId: validateHeavyFamilyPlacement
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LanePlacementValidationRequest'
responses:
'200':
description: Placement validation result.
content:
application/json:
schema:
$ref: '#/components/schemas/LanePlacementValidationResult'
/test-lanes/{laneId}/reports/heavy-attribution/latest:
get:
summary: Read the latest heavy-attribution report for a lane.
operationId: getLatestHeavyAttributionReport
parameters:
- name: laneId
in: path
required: true
schema:
$ref: '#/components/schemas/LaneId'
responses:
'200':
description: Latest heavy-attribution report for the requested lane.
content:
application/json:
schema:
$ref: '#/components/schemas/HeavyAttributionReport'
components:
schemas:
ClassificationId:
type: string
enum:
- ui-light
- ui-workflow
- surface-guard
- discovery-heavy
- browser
LaneId:
type: string
enum:
- fast-feedback
- confidence
- heavy-governance
- browser
- profiling
- junit
PlacementAllowance:
type: string
enum:
- required
- allowed
- discouraged
- forbidden
ValidationStatus:
type: string
enum:
- seeded
- reviewed
- migrated
- guarded
BudgetStatus:
type: string
enum:
- within-budget
- warning
- over-budget
BudgetBaselineSource:
type: string
enum:
- measured-current-suite
- measured-lane
- measured-post-spec-207
BudgetEnforcement:
type: string
enum:
- report-only
- warn
- hard-fail
BudgetLifecycleState:
type: string
enum:
- draft
- measured
- documented
- enforced
HeavyFamilySelector:
type: object
additionalProperties: false
required:
- selectorType
- selectorValue
- selectorRole
- sourceOfTruth
properties:
selectorType:
type: string
enum:
- suite
- path
- group
- file
selectorValue:
type: string
minLength: 1
selectorRole:
type: string
enum:
- include
- exclude
- inventory-only
sourceOfTruth:
type: string
enum:
- manifest
- pest-group
- guard-test
- report-attribution
rationale:
type: string
HeavyTestClassification:
type: object
additionalProperties: false
required:
- classificationId
- purpose
- dominantCostDrivers
- defaultLaneId
- allowedLaneIds
- forbiddenLaneIds
- reviewerSignals
- escalationTriggers
properties:
classificationId:
$ref: '#/components/schemas/ClassificationId'
purpose:
type: string
dominantCostDrivers:
type: array
minItems: 1
items:
type: string
defaultLaneId:
$ref: '#/components/schemas/LaneId'
allowedLaneIds:
type: array
minItems: 1
items:
$ref: '#/components/schemas/LaneId'
forbiddenLaneIds:
type: array
items:
$ref: '#/components/schemas/LaneId'
reviewerSignals:
type: array
minItems: 1
items:
type: string
escalationTriggers:
type: array
minItems: 1
items:
type: string
allOf:
- if:
properties:
classificationId:
const: browser
then:
properties:
defaultLaneId:
const: browser
allowedLaneIds:
type: array
minItems: 1
maxItems: 1
items:
const: browser
- if:
properties:
classificationId:
const: surface-guard
then:
properties:
defaultLaneId:
const: heavy-governance
forbiddenLaneIds:
allOf:
- contains:
const: fast-feedback
- contains:
const: confidence
- if:
properties:
classificationId:
const: ui-workflow
then:
properties:
defaultLaneId:
const: confidence
- if:
properties:
classificationId:
const: discovery-heavy
then:
properties:
defaultLaneId:
const: heavy-governance
forbiddenLaneIds:
allOf:
- contains:
const: fast-feedback
- contains:
const: confidence
HeavyTestFamily:
type: object
additionalProperties: false
required:
- familyId
- classificationId
- purpose
- currentLaneId
- targetLaneId
- selectors
- hotspotFiles
- costSignals
- validationStatus
properties:
familyId:
type: string
classificationId:
$ref: '#/components/schemas/ClassificationId'
purpose:
type: string
currentLaneId:
$ref: '#/components/schemas/LaneId'
targetLaneId:
$ref: '#/components/schemas/LaneId'
selectors:
type: array
minItems: 1
items:
$ref: '#/components/schemas/HeavyFamilySelector'
hotspotFiles:
type: array
minItems: 1
items:
type: string
costSignals:
type: array
minItems: 1
items:
type: string
confidenceRationale:
type: string
validationStatus:
$ref: '#/components/schemas/ValidationStatus'
allOf:
- if:
properties:
targetLaneId:
const: confidence
then:
required:
- confidenceRationale
properties:
confidenceRationale:
type: string
minLength: 1
- if:
properties:
classificationId:
const: ui-light
then:
properties:
targetLaneId:
enum:
- fast-feedback
- confidence
- if:
properties:
classificationId:
const: ui-workflow
then:
properties:
targetLaneId:
enum:
- confidence
- heavy-governance
- if:
properties:
classificationId:
const: surface-guard
then:
properties:
targetLaneId:
const: heavy-governance
- if:
properties:
classificationId:
const: discovery-heavy
then:
properties:
targetLaneId:
const: heavy-governance
- if:
properties:
classificationId:
const: browser
then:
properties:
targetLaneId:
const: browser
MixedFileResolution:
type: object
additionalProperties: false
required:
- filePath
- primaryClassificationId
- resolutionStrategy
- rationale
- followUpRequired
properties:
filePath:
type: string
minLength: 1
primaryClassificationId:
$ref: '#/components/schemas/ClassificationId'
secondaryClassificationIds:
type: array
items:
$ref: '#/components/schemas/ClassificationId'
resolutionStrategy:
type: string
enum:
- split-file
- broadest-cost-wins
rationale:
type: string
minLength: 1
followUpRequired:
type: boolean
LanePlacementRule:
type: object
additionalProperties: false
required:
- ruleId
- classificationId
- laneId
- allowance
- reason
properties:
ruleId:
type: string
classificationId:
$ref: '#/components/schemas/ClassificationId'
laneId:
$ref: '#/components/schemas/LaneId'
allowance:
$ref: '#/components/schemas/PlacementAllowance'
reason:
type: string
exceptionPolicy:
type: string
LanePlacementValidationRequest:
type: object
additionalProperties: false
required:
- laneId
properties:
laneId:
$ref: '#/components/schemas/LaneId'
classificationId:
$ref: '#/components/schemas/ClassificationId'
familyId:
type: string
filePath:
type: string
observedCostSignals:
type: array
items:
type: string
anyOf:
- required:
- classificationId
- required:
- familyId
- required:
- filePath
LanePlacementValidationResult:
type: object
additionalProperties: false
required:
- laneId
- resolvedClassificationId
- allowance
- valid
- reasons
properties:
laneId:
$ref: '#/components/schemas/LaneId'
resolvedClassificationId:
$ref: '#/components/schemas/ClassificationId'
familyId:
type: string
allowance:
$ref: '#/components/schemas/PlacementAllowance'
valid:
type: boolean
reasons:
type: array
minItems: 1
items:
type: string
remediationOptions:
type: array
items:
type: string
mixedFileResolution:
$ref: '#/components/schemas/MixedFileResolution'
HeavyBudgetEvaluation:
type: object
additionalProperties: false
required:
- targetType
- targetId
- thresholdSeconds
- measuredSeconds
- baselineSource
- enforcement
- lifecycleState
- status
properties:
targetType:
type: string
enum:
- lane
- classification
- family
targetId:
type: string
thresholdSeconds:
type: integer
minimum: 1
measuredSeconds:
type: number
minimum: 0
baselineSource:
$ref: '#/components/schemas/BudgetBaselineSource'
enforcement:
$ref: '#/components/schemas/BudgetEnforcement'
lifecycleState:
$ref: '#/components/schemas/BudgetLifecycleState'
status:
$ref: '#/components/schemas/BudgetStatus'
HeavyAttributionReport:
type: object
additionalProperties: false
required:
- laneId
- finishedAt
- wallClockSeconds
- slowestEntries
- classificationAttribution
- familyAttribution
- budgetEvaluations
- artifactDirectory
properties:
laneId:
$ref: '#/components/schemas/LaneId'
finishedAt:
type: string
format: date-time
wallClockSeconds:
type: number
minimum: 0
slowestEntries:
type: array
description: Canonical top 10 runtime hotspot entries ordered by slowest wall-clock time.
minItems: 10
items:
type: object
additionalProperties: false
required:
- label
- wallClockSeconds
properties:
label:
type: string
wallClockSeconds:
type: number
minimum: 0
classificationAttribution:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- classificationId
- totalWallClockSeconds
properties:
classificationId:
$ref: '#/components/schemas/ClassificationId'
totalWallClockSeconds:
type: number
minimum: 0
hotspotFiles:
type: array
items:
type: string
familyAttribution:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- familyId
- classificationId
- totalWallClockSeconds
properties:
familyId:
type: string
classificationId:
$ref: '#/components/schemas/ClassificationId'
totalWallClockSeconds:
type: number
minimum: 0
hotspotFiles:
type: array
items:
type: string
budgetEvaluations:
type: array
minItems: 1
allOf:
- contains:
type: object
required:
- targetType
- targetId
properties:
targetType:
const: lane
targetId:
const: heavy-governance
- contains:
type: object
required:
- targetType
properties:
targetType:
const: classification
- contains:
type: object
required:
- targetType
properties:
targetType:
const: family
items:
$ref: '#/components/schemas/HeavyBudgetEvaluation'
artifactDirectory:
type: string
const: storage/logs/test-lanes

View File

@ -0,0 +1,809 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://tenantatlas.local/schemas/heavy-test-classification.schema.json",
"title": "HeavyTestClassificationCatalog",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"artifactDirectory",
"classifications",
"families",
"mixedFileResolutions",
"placementRules",
"driftGuards",
"budgetTargets"
],
"properties": {
"version": {
"type": "integer",
"minimum": 1
},
"artifactDirectory": {
"type": "string",
"const": "storage/logs/test-lanes"
},
"classifications": {
"type": "array",
"minItems": 5,
"items": {
"$ref": "#/$defs/classification"
},
"allOf": [
{
"contains": {
"type": "object",
"required": ["classificationId"],
"properties": {
"classificationId": {
"const": "ui-light"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["classificationId"],
"properties": {
"classificationId": {
"const": "ui-workflow"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["classificationId"],
"properties": {
"classificationId": {
"const": "surface-guard"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["classificationId"],
"properties": {
"classificationId": {
"const": "discovery-heavy"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["classificationId"],
"properties": {
"classificationId": {
"const": "browser"
}
}
}
}
]
},
"families": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/family"
}
},
"mixedFileResolutions": {
"type": "array",
"items": {
"$ref": "#/$defs/mixedFileResolution"
}
},
"placementRules": {
"type": "array",
"minItems": 5,
"items": {
"$ref": "#/$defs/placementRule"
}
},
"driftGuards": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/driftGuard"
},
"allOf": [
{
"contains": {
"type": "object",
"required": [
"targetRefs"
],
"properties": {
"targetRefs": {
"type": "array",
"contains": {
"const": "browser"
}
}
}
}
},
{
"contains": {
"type": "object",
"required": [
"targetRefs"
],
"properties": {
"targetRefs": {
"type": "array",
"contains": {
"const": "discovery-heavy"
}
}
}
}
},
{
"contains": {
"type": "object",
"required": [
"targetRefs"
],
"properties": {
"targetRefs": {
"type": "array",
"contains": {
"const": "surface-guard"
}
}
}
}
}
]
},
"budgetTargets": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/budgetTarget"
},
"allOf": [
{
"contains": {
"type": "object",
"required": [
"targetType",
"targetId"
],
"properties": {
"targetType": {
"const": "lane"
},
"targetId": {
"const": "heavy-governance"
}
}
}
},
{
"contains": {
"type": "object",
"required": [
"targetType"
],
"properties": {
"targetType": {
"const": "classification"
}
}
}
},
{
"contains": {
"type": "object",
"required": [
"targetType"
],
"properties": {
"targetType": {
"const": "family"
}
}
}
}
]
}
},
"$defs": {
"classificationId": {
"type": "string",
"enum": [
"ui-light",
"ui-workflow",
"surface-guard",
"discovery-heavy",
"browser"
]
},
"laneId": {
"type": "string",
"enum": [
"fast-feedback",
"confidence",
"heavy-governance",
"browser",
"profiling",
"junit"
]
},
"selector": {
"type": "object",
"additionalProperties": false,
"required": [
"selectorType",
"selectorValue",
"selectorRole",
"sourceOfTruth"
],
"properties": {
"selectorType": {
"type": "string",
"enum": [
"suite",
"path",
"group",
"file"
]
},
"selectorValue": {
"type": "string",
"minLength": 1
},
"selectorRole": {
"type": "string",
"enum": [
"include",
"exclude",
"inventory-only"
]
},
"sourceOfTruth": {
"type": "string",
"enum": [
"manifest",
"pest-group",
"guard-test",
"report-attribution"
]
},
"rationale": {
"type": "string"
}
}
},
"classification": {
"type": "object",
"additionalProperties": false,
"required": [
"classificationId",
"purpose",
"dominantCostDrivers",
"defaultLaneId",
"allowedLaneIds",
"forbiddenLaneIds",
"reviewerSignals",
"escalationTriggers"
],
"properties": {
"classificationId": {
"$ref": "#/$defs/classificationId"
},
"purpose": {
"type": "string",
"minLength": 1
},
"dominantCostDrivers": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"defaultLaneId": {
"$ref": "#/$defs/laneId"
},
"allowedLaneIds": {
"$ref": "#/$defs/nonEmptyLaneArray"
},
"forbiddenLaneIds": {
"type": "array",
"items": {
"$ref": "#/$defs/laneId"
},
"uniqueItems": true
},
"reviewerSignals": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"escalationTriggers": {
"$ref": "#/$defs/nonEmptyStringArray"
}
},
"allOf": [
{
"if": {
"properties": {
"classificationId": {
"const": "browser"
}
}
},
"then": {
"properties": {
"defaultLaneId": {
"const": "browser"
},
"allowedLaneIds": {
"type": "array",
"minItems": 1,
"maxItems": 1,
"items": {
"const": "browser"
}
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "ui-workflow"
}
}
},
"then": {
"properties": {
"defaultLaneId": {
"const": "confidence"
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "surface-guard"
}
}
},
"then": {
"properties": {
"defaultLaneId": {
"const": "heavy-governance"
},
"forbiddenLaneIds": {
"allOf": [
{
"contains": {
"const": "fast-feedback"
}
},
{
"contains": {
"const": "confidence"
}
}
]
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "discovery-heavy"
}
}
},
"then": {
"properties": {
"defaultLaneId": {
"const": "heavy-governance"
},
"forbiddenLaneIds": {
"allOf": [
{
"contains": {
"const": "fast-feedback"
}
},
{
"contains": {
"const": "confidence"
}
}
]
}
}
}
}
]
},
"family": {
"type": "object",
"additionalProperties": false,
"required": [
"familyId",
"classificationId",
"purpose",
"currentLaneId",
"targetLaneId",
"selectors",
"hotspotFiles",
"costSignals",
"validationStatus"
],
"properties": {
"familyId": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"classificationId": {
"$ref": "#/$defs/classificationId"
},
"purpose": {
"type": "string",
"minLength": 1
},
"currentLaneId": {
"$ref": "#/$defs/laneId"
},
"targetLaneId": {
"$ref": "#/$defs/laneId"
},
"selectors": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/selector"
}
},
"hotspotFiles": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"costSignals": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"confidenceRationale": {
"type": "string"
},
"validationStatus": {
"type": "string",
"enum": [
"seeded",
"reviewed",
"migrated",
"guarded"
]
}
},
"allOf": [
{
"if": {
"properties": {
"targetLaneId": {
"const": "confidence"
}
}
},
"then": {
"required": [
"confidenceRationale"
],
"properties": {
"confidenceRationale": {
"type": "string",
"minLength": 1
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "ui-light"
}
}
},
"then": {
"properties": {
"targetLaneId": {
"enum": [
"fast-feedback",
"confidence"
]
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "ui-workflow"
}
}
},
"then": {
"properties": {
"targetLaneId": {
"enum": [
"confidence",
"heavy-governance"
]
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "surface-guard"
}
}
},
"then": {
"properties": {
"targetLaneId": {
"const": "heavy-governance"
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "discovery-heavy"
}
}
},
"then": {
"properties": {
"targetLaneId": {
"const": "heavy-governance"
}
}
}
},
{
"if": {
"properties": {
"classificationId": {
"const": "browser"
}
}
},
"then": {
"properties": {
"targetLaneId": {
"const": "browser"
}
}
}
}
]
},
"mixedFileResolution": {
"type": "object",
"additionalProperties": false,
"required": [
"filePath",
"primaryClassificationId",
"resolutionStrategy",
"rationale",
"followUpRequired"
],
"properties": {
"filePath": {
"type": "string",
"minLength": 1
},
"primaryClassificationId": {
"$ref": "#/$defs/classificationId"
},
"secondaryClassificationIds": {
"type": "array",
"items": {
"$ref": "#/$defs/classificationId"
},
"uniqueItems": true
},
"resolutionStrategy": {
"type": "string",
"enum": [
"split-file",
"broadest-cost-wins"
]
},
"rationale": {
"type": "string",
"minLength": 1
},
"followUpRequired": {
"type": "boolean"
}
}
},
"placementRule": {
"type": "object",
"additionalProperties": false,
"required": [
"ruleId",
"classificationId",
"laneId",
"allowance",
"reason"
],
"properties": {
"ruleId": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"classificationId": {
"$ref": "#/$defs/classificationId"
},
"laneId": {
"$ref": "#/$defs/laneId"
},
"allowance": {
"type": "string",
"enum": [
"required",
"allowed",
"discouraged",
"forbidden"
]
},
"reason": {
"type": "string",
"minLength": 1
},
"exceptionPolicy": {
"type": "string"
}
}
},
"driftGuard": {
"type": "object",
"additionalProperties": false,
"required": [
"guardId",
"scope",
"assertionType",
"targetRefs",
"owningTestPaths",
"failureContract"
],
"properties": {
"guardId": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"scope": {
"type": "string",
"enum": [
"classification",
"family",
"lane",
"report"
]
},
"assertionType": {
"type": "string",
"enum": [
"forbidden-membership",
"required-membership",
"required-attribution",
"required-selector",
"browser-isolation"
]
},
"targetRefs": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"owningTestPaths": {
"$ref": "#/$defs/nonEmptyStringArray"
},
"failureContract": {
"type": "string",
"minLength": 1
}
}
},
"budgetTarget": {
"type": "object",
"additionalProperties": false,
"required": [
"budgetId",
"targetType",
"targetId",
"thresholdSeconds",
"baselineSource",
"enforcement",
"lifecycleState"
],
"properties": {
"budgetId": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"targetType": {
"type": "string",
"enum": [
"lane",
"classification",
"family"
]
},
"targetId": {
"type": "string",
"minLength": 1
},
"thresholdSeconds": {
"type": "integer",
"minimum": 1
},
"baselineSource": {
"type": "string",
"enum": [
"measured-current-suite",
"measured-lane",
"measured-post-spec-207"
]
},
"enforcement": {
"type": "string",
"enum": [
"report-only",
"warn",
"hard-fail"
]
},
"lifecycleState": {
"type": "string",
"enum": [
"draft",
"measured",
"documented",
"enforced"
]
},
"reviewCadence": {
"type": "string"
}
}
},
"nonEmptyStringArray": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
},
"uniqueItems": true
},
"nonEmptyLaneArray": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/laneId"
},
"uniqueItems": true
}
}
}

View File

@ -0,0 +1,206 @@
# Data Model: Filament/Livewire Heavy Suite Segmentation
This feature does not introduce new runtime database tables. The data-model work formalizes repository-level governance objects that describe how heavy Filament, Livewire, surface, discovery, and browser families are classified, assigned to lanes, guarded against drift, and attributed in reporting. It builds directly on the existing Spec 206 lane manifest and Spec 207 fixture-cost outputs.
## 1. Heavy Test Classification
### Purpose
Represents a repository-defined cost and purpose class for a UI-related test family.
### Fields
- `classification_id`: stable identifier such as `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, or `browser`
- `purpose`: contributor-facing description of what this class is meant to cover
- `dominant_cost_drivers`: list of characteristics such as `single-mount`, `multi-mount`, `relation-manager breadth`, `reflection`, `resource discovery`, `workflow fan-out`, or `real-browser interaction`
- `default_lane_id`: primary execution lane for this class
- `allowed_lane_ids`: lanes where the class may legitimately appear
- `forbidden_lane_ids`: lanes where the class must not appear
- `reviewer_signals`: short list of clues a reviewer should look for when classifying a new test
- `escalation_triggers`: conditions that force a test out of a lighter class, such as broad discovery, wide reflection, or repeated multi-surface mounts
### Validation rules
- `browser` must allow only the `browser` lane.
- `discovery-heavy` must forbid `fast-feedback` and `confidence`.
- `surface-guard` must forbid `fast-feedback` and `confidence` and default to `heavy-governance`.
- `ui-light` may appear in `fast-feedback` and `confidence` only when its dominant cost drivers remain localized.
- `ui-workflow` defaults to `confidence` and may escalate to `heavy-governance` if its breadth becomes governance-wide.
## 2. Heavy Test Family
### Purpose
Represents a named cluster of related tests that share a heavy behavior pattern, hotspot files, and intended lane placement.
### Fields
- `family_id`: stable identifier for the family
- `classification_id`: owning heavy class
- `purpose`: why the family exists and what safety it provides
- `current_lane_id`: the lane where the family currently runs
- `target_lane_id`: the intended lane after segmentation
- `selectors`: one or more selectors that identify the family in the manifest or guards
- `hotspot_files`: representative or dominant files in the family
- `cost_signals`: concrete evidence of heaviness such as reflection use, multi-page mounts, relation-manager breadth, concern-driven fixture graphs, or repeated action-surface assertions
- `confidence_rationale`: explanation of why the family should remain in Confidence or move to Heavy Governance
- `validation_status`: `seeded`, `reviewed`, `migrated`, or `guarded`
### Validation rules
- Every family must have at least one selector and one hotspot file.
- `target_lane_id` must be compatible with the owning `classification_id`.
- Families assigned to `fast-feedback` may not carry `discovery-heavy` or broad `surface-guard` signals.
- A family with `classification_id = browser` must target the `browser` lane only.
- Families assigned to `confidence` must include `confidence_rationale` explaining why the retained UI safety is worth the lane cost.
## 3. Heavy Family Selector
### Purpose
Represents the checked-in selector used to find a heavy family during lane execution and guard validation.
### Fields
- `selector_type`: `suite`, `path`, `group`, or `file`
- `selector_value`: exact selector value used by the manifest or guard
- `selector_role`: `include`, `exclude`, or `inventory-only`
- `rationale`: why this selector is needed
- `source_of_truth`: `manifest`, `pest-group`, `guard-test`, or `report-attribution`
### Validation rules
- Selectors used for execution must be representable in `TestLaneManifest`.
- Families with more than one hotspot should not rely on a single ad-hoc file selector alone unless the file is intentionally the whole family.
## 4. Lane Placement Rule
### Purpose
Describes the allowed relationship between a heavy class and an execution lane.
### Fields
- `rule_id`: stable identifier
- `classification_id`: heavy class under review
- `lane_id`: execution lane
- `allowance`: `required`, `allowed`, `discouraged`, or `forbidden`
- `reason`: concise explanation of the rule
- `exception_policy`: how rare justified exceptions are handled
### Validation rules
- Each heavy class must have exactly one `required` or `default` lane target.
- `browser` cannot be `allowed` in non-browser lanes.
- `discovery-heavy` and broad `surface-guard` classes must be `forbidden` in `fast-feedback`.
- `confidence` may contain only the classes explicitly marked `allowed` or `required` there.
## 5. Mixed File Resolution
### Purpose
Represents how a file that exhibits more than one heavy behavior is resolved for lane placement.
### Fields
- `file_path`: relative test file path
- `primary_classification_id`: the classification that drives lane placement
- `secondary_classification_ids`: additional observed behaviors
- `resolution_strategy`: `split-file` or `broadest-cost-wins`
- `rationale`: why the chosen resolution is acceptable
- `follow_up_required`: boolean
### Validation rules
- Files with `resolution_strategy = broadest-cost-wins` must have an explicit rationale.
- Files whose primary classification is `discovery-heavy` or `surface-guard` may not remain in `fast-feedback` or `confidence`.
## 6. Drift Guard
### Purpose
Represents a checked-in validation that prevents wrong-lane placement for heavy classes or families.
### Fields
- `guard_id`: stable identifier
- `scope`: `classification`, `family`, `lane`, or `report`
- `assertion_type`: `forbidden-membership`, `required-membership`, `required-attribution`, `required-selector`, or `browser-isolation`
- `target_refs`: classes, families, files, or lanes covered by the guard
- `failure_contract`: the minimum actionable failure output expected from the guard
- `owning_test_paths`: tests that enforce the guard
### Validation rules
- Every forbidden lane relationship for `browser`, `discovery-heavy`, and broad `surface-guard` must have at least one drift guard.
- Drift guards must fail with actionable output that identifies the violating file or family.
## 7. Heavy Budget Threshold
### Purpose
Represents the runtime budget target for a heavy lane, heavy class, or heavy family.
### Fields
- `budget_id`: stable identifier
- `target_type`: `lane`, `classification`, or `family`
- `target_id`: referenced lane, classification, or family
- `threshold_seconds`: allowed wall-clock target
- `baseline_source`: `measured-current-suite`, `measured-lane`, or `measured-post-spec-207`
- `enforcement_level`: `report-only`, `warn`, or `hard-fail`
- `lifecycle_state`: `draft`, `measured`, `documented`, or `enforced`
- `review_cadence`: how often the budget should be revisited
### Validation rules
- Heavy Governance must retain a lane-level budget.
- At least one heavy family budget must exist beyond the lane total.
- New family budgets should stay report-oriented until lane baselines stabilize.
## 8. Heavy Attribution Report
### Purpose
Represents the reporting view that explains heavy-lane cost by class and family.
### Fields
- `lane_id`: lane being reported
- `wall_clock_seconds`: total lane duration
- `slowest_entries`: canonical top 10 runtime hotspot entries ordered by slowest wall-clock time
- `classification_attribution`: totals grouped by heavy class
- `family_attribution`: totals grouped by heavy family
- `budget_evaluations`: lane, classification, and family budget evaluations
- `artifact_paths`: generated report artifacts
### Validation rules
- Heavy reports must retain the canonical top 10 runtime hotspot view ordered by slowest wall-clock time.
- If a heavy family is cataloged, its cost should be attributable either directly or through its classification.
- Report artifact paths must remain under the app-root contract path `storage/logs/test-lanes`.
## 9. First-Slice Governance Inventory
### Existing lane infrastructure
- Operational lanes already present: `fast-feedback`, `confidence`, `browser`, and `heavy-governance`
- Support-lane entries already present: `profiling` and `junit`
- Existing family budgets currently cover `ops-ux-governance` and `browser-smoke`
### Initial heavy family clusters
- `tests/Feature/Filament/`: broadest mixed UI cluster; currently contains UI-Light, UI-Workflow, Surface-Guard, and Discovery-Heavy candidates
- `tests/Feature/Rbac/`: relation-manager and role-matrix heavy families with repeated Livewire mounts and capability transitions
- `tests/Feature/Baselines/` plus related Filament baseline pages: dense workflow and matrix-style coverage with expensive fixture construction
- `tests/Feature/Concerns/`: heavy builder traits such as baseline compare and portfolio triage fixtures that amplify test cost across families
- `tests/Feature/Guards/ActionSurfaceContractTest.php` and related governance guards: canonical Surface-Guard seeds already treated as heavy
- `tests/Browser/`: isolated Browser class, already a separate lane and cost domain
### Initial heavy signals
- Reflection-based discovery such as `ReflectionProperty` and global-search parity checks
- Multi-mount Livewire tests across pages, relation managers, and actions
- Broad action-surface and navigation-discipline validation
- Concern-driven fixture construction that creates dense comparison or triage graphs
- Browser interaction and end-to-end smoke coverage

View File

@ -0,0 +1,168 @@
# Implementation Plan: Filament/Livewire Heavy Suite Segmentation
**Branch**: `208-heavy-suite-segmentation` | **Date**: 2026-04-16 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/208-heavy-suite-segmentation/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/208-heavy-suite-segmentation/spec.md`
## Summary
Build Spec 208 on top of the existing Spec 206 lane manifest and the Spec 207 fixture-cost work by introducing a repo-local heavy UI classification catalog, inventorying the current Filament-, Livewire-, surface-, discovery-, and workflow-heavy families, tightening manifest selectors and Pest groups around those families, extending guard coverage so heavy tests cannot drift into the wrong lanes, and adding class- and family-level attribution to heavy-lane reporting without introducing new runtime services or product-facing behavior.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
**Storage**: SQLite `:memory:` for the default test environment, existing lane artifacts under the app-root contract path `storage/logs/test-lanes`, and no new product persistence
**Testing**: Pest unit, feature, browser, architecture, and guard suites run through Sail-wrapped `artisan test`; lane selection is currently controlled by `Tests\Support\TestLaneManifest`, Pest groups in `tests/Pest.php`, and focused guard tests under `tests/Feature/Guards`
**Target Platform**: Laravel monorepo application in `apps/platform`, executed locally through Sail and later hardened in shared CI
**Project Type**: Monorepo with a Laravel platform app and separate Astro website; this feature is scoped to platform test-governance infrastructure
**Performance Goals**: Keep Fast Feedback free of Discovery-Heavy and broad Surface-Guard families, keep Confidence limited to documented UI-Light and selected UI-Workflow families, make Heavy Governance attributable by heavy class and family, and preserve or improve the post-Spec 207 fast-feedback and confidence baselines while keeping the heavy lane observable
**Constraints**: Sail-first commands only; no new product routes, assets, runtime services, or dependencies; Browser remains a separate lane; the classification model must stay small and evidence-driven; directory names alone cannot be the source of truth; mixed files must be split or classified by their broadest cost driver
**Scale/Scope**: Existing lane infrastructure already defines six checked-in lane entries; heavy families currently concentrate in `tests/Feature/Filament`, `tests/Feature/Rbac`, `tests/Feature/Baselines`, `tests/Feature/Concerns`, broad governance guards, and `tests/Browser`; the exploratory inventory identified roughly 209 Filament feature tests, about 35 RBAC-heavy files, and dozens of Baseline Compare and governance-adjacent tests that need explicit segmentation
### Filament v5 Implementation Notes
- **Livewire v4.0+ compliance**: Preserved. This feature changes only repository test-governance around Filament and Livewire surfaces, not runtime Filament or Livewire behavior.
- **Provider registration location**: Unchanged. Existing panel providers remain registered in `bootstrap/providers.php`.
- **Global search rule**: No globally searchable resources are added or modified. Tests may classify global-search parity checks as heavy discovery families, but runtime global-search behavior is unchanged.
- **Destructive actions**: No runtime destructive actions are introduced. Any new tests added by this feature continue to validate existing confirmation and authorization behavior.
- **Asset strategy**: No panel-only or shared assets are added. Existing `filament:assets` deployment behavior remains unchanged.
- **Testing plan**: Add Pest guard coverage for heavy-classification catalog validity, lane-to-class mapping, heavy-family drift detection, report attribution, and targeted validation of the resegmented Fast Feedback, Confidence, and Heavy Governance lanes.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: PASS. No inventory, snapshot, or backup truth is changed.
- Read/write separation: PASS. The feature only changes repository test-governance behavior and introduces no end-user mutation path.
- Graph contract path: PASS. No Graph calls, contract-registry changes, or provider runtime integrations are added.
- Deterministic capabilities: PASS. No capability resolver or authorization registry changes.
- RBAC-UX, workspace isolation, tenant isolation: PASS. No runtime routes, policies, global search availability, or tenant/workspace enforcement semantics are changed.
- Run observability and Ops-UX: PASS. Reporting remains filesystem-based through the existing lane tooling and does not introduce `OperationRun` behavior.
- Data minimization: PASS. Heavy-family inventories and lane reports remain repo-local and contain no secrets or customer payloads.
- Proportionality and bloat control: PASS WITH LIMITS. The only new semantic layer is a narrow repo-local classification catalog for heavy UI test families. The plan explicitly avoids a broader meta-framework and keeps lane placement tied to measurable cost and purpose.
- TEST-TRUTH-001: PASS. The feature increases suite honesty by making broad discovery, surface, and workflow costs visible and governable instead of leaving them hidden in general feature lanes.
- Filament/UI constitutions: PASS / NOT APPLICABLE. No operator-facing UI, action-surface runtime contract, badge semantics, or panel IA is changed.
**Phase 0 Gate Result**: PASS
- The feature remains bounded to repository test governance, reporting, selector rules, and author guidance.
- No new runtime persistence, product routes, panels, assets, or Graph seams are introduced.
- The chosen approach extends the existing Spec 206/207 tooling instead of creating a second test-governance system.
## Project Structure
### Documentation (this feature)
```text
specs/208-heavy-suite-segmentation/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ ├── heavy-test-classification.schema.json
│ └── heavy-suite-segmentation.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/
├── platform/
│ ├── composer.json
│ ├── tests/
│ │ ├── Pest.php
│ │ ├── Support/
│ │ │ ├── TestLaneManifest.php
│ │ │ ├── TestLaneBudget.php
│ │ │ └── TestLaneReport.php
│ │ ├── Feature/
│ │ │ ├── Baselines/
│ │ │ ├── Concerns/
│ │ │ ├── Filament/
│ │ │ ├── Guards/
│ │ │ ├── Navigation/
│ │ │ ├── Onboarding/
│ │ │ └── Rbac/
│ │ ├── Browser/
│ │ └── Unit/
│ └── storage/logs/test-lanes/
├── website/
└── ...
scripts/
├── platform-test-lane
└── platform-test-report
```
**Structure Decision**: Keep implementation concentrated in the existing platform test-governance seams: `apps/platform/tests/Pest.php` for group tagging, `apps/platform/tests/Support/TestLaneManifest.php` for lane catalog and family metadata, `apps/platform/tests/Support/TestLaneReport.php` for attribution output, targeted guard tests under `apps/platform/tests/Feature/Guards`, and selective heavy-family files under `apps/platform/tests/Feature/Filament`, `apps/platform/tests/Feature/Rbac`, `apps/platform/tests/Feature/Baselines`, and `apps/platform/tests/Feature/Concerns`. Planning artifacts stay inside `specs/208-heavy-suite-segmentation`.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: Contributors and reviewers cannot reliably see when a Filament or Livewire test family is too broad for Fast Feedback or Confidence, so expensive discovery and surface-guard behavior silently erodes faster lanes.
- **Existing structure is insufficient because**: The current lane manifest distinguishes fast, confidence, browser, and heavy-governance at a coarse level but does not classify heavy UI families by their actual cost drivers or preserve their intent for reviewers.
- **Narrowest correct implementation**: Extend the existing lane manifest, Pest group seams, guard tests, and report attribution with a small heavy-family catalog and class model instead of adding a new runner, new persistence, or a broad test taxonomy framework.
- **Ownership cost created**: The repo must maintain the heavy classification catalog, targeted family inventory, lane-to-class rules, drift guards, and budget attribution as new UI-heavy tests are added.
- **Alternative intentionally rejected**: Purely directory-based moves or ad-hoc per-file exclusions, because they hide cost instead of making it reviewable and enforceable.
- **Release truth**: Current-release repository truth and the direct next step after Specs 206 and 207.
## Phase 0 — Research (complete)
- Output: [research.md](./research.md)
- Resolved key decisions:
- Reuse the existing `TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams instead of introducing a new lane engine.
- Model heavy UI cost with five explicit classes: `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, and `browser`.
- Keep `heavy-governance` as the operational destination for broad Surface-Guard and Discovery-Heavy families instead of adding a brand-new operational lane.
- Represent heavy-family ownership through a hybrid of manifest selectors, granular Pest groups, and explicit hotspot file inventory rather than forcing a large directory move before the classification model stabilizes.
- Treat mixed files as a first-class case: either split them or classify them by the broadest cost-driving behavior.
- Start the heavy-family inventory from existing hotspots in `tests/Feature/Filament`, `tests/Feature/Rbac`, `tests/Feature/Baselines`, `tests/Feature/Concerns`, and the existing Browser lane, then tighten placement from profiling evidence.
- Extend drift guards from coarse lane exclusion to explicit class-to-lane validation so new heavy tests cannot silently diffuse into Fast Feedback or Confidence.
- Extend report output and family budgets to attribute heavy costs by class and family, not only by lane.
## Phase 1 — Design & Contracts (complete)
- Output: [data-model.md](./data-model.md) formalizes the heavy classification catalog, heavy family inventory, lane placement rules, mixed-file handling, drift guards, and report attribution model.
- Output: [contracts/heavy-test-classification.schema.json](./contracts/heavy-test-classification.schema.json) defines the checked-in schema for the heavy classification catalog, family inventory, confidence rationale requirements, and mixed-file resolution records.
- Output: [contracts/heavy-suite-segmentation.logical.openapi.yaml](./contracts/heavy-suite-segmentation.logical.openapi.yaml) captures the logical contract for classifying heavy families, reading family and mixed-file inventories, validating lane placement, and reading heavy-attribution reports.
- Output: [quickstart.md](./quickstart.md) provides the rollout order, validation commands, and review checkpoints for the segmentation work.
### Post-design Constitution Re-check
- PASS: No runtime routes, panels, authorization planes, or Graph seams are introduced.
- PASS: The new classification catalog is repo-local, directly justified by current suite cost, and bounded to existing lane-governance infrastructure.
- PASS: The design prefers extending existing manifest, guard, and reporting seams over adding a second governance framework.
- PASS WITH WORK: The heavy-family inventory must remain evidence-driven and limited to families that materially change lane cost or review behavior.
- PASS WITH WORK: Confidence preservation must be documented explicitly as families move so the faster lane mix does not become a hollow smoke-only layer.
## Phase 2 — Implementation Planning
`tasks.md` should cover:
- Inventorying the current heavy Filament, Livewire, surface, discovery, workflow, wizard, header-action, and navigation-discipline families touched by current lanes.
- Extending `TestLaneManifest` with a checked-in heavy-classification catalog and explicit heavy-family inventory.
- Adding granular Pest groups or equivalent checked-in metadata for `ui-light`, `ui-workflow`, `surface-guard`, and `discovery-heavy` where group-based selection provides better drift control.
- Refining Fast Feedback and Confidence selectors so Discovery-Heavy and broad Surface-Guard families are excluded intentionally rather than by scattered file exceptions.
- Reassigning the first heavy hotspot families from coarse exclusions into explicit `heavy-governance` family ownership, including action-surface, header-action, navigation-discipline, relation-manager, wizard, and discovery-heavy hotspots.
- Extending `TestLaneBudget` and `TestLaneReport` so heavy budgets and slowest entries are attributable by class and family, not only by lane.
- Adding or expanding guard tests that verify class-to-lane rules, browser isolation, mixed-family handling, and heavy-family drift detection.
- Validating that Confidence still contains the intended UI-Light and selected UI-Workflow safety after the moves.
- Running the existing lane wrappers to compare Fast Feedback, Confidence, and Heavy Governance behavior against the post-Spec 207 baselines.
- Publishing concise author and reviewer guidance for new heavy UI tests so placement intent is visible at review time.
### Contract Implementation Note
- The JSON schema is schema-first and repository-tooling-oriented. It defines what the checked-in heavy classification catalog, family inventory, confidence rationale, and mixed-file resolution records must express even if the first implementation remains PHP arrays in `TestLaneManifest`.
- The OpenAPI file is logical rather than transport-prescriptive. It documents the expected semantics of heavy-family classification, mixed-file resolution, lane-validation, and report-attribution flows for in-process repository tooling.
- The plan intentionally avoids introducing a new runtime service, a new database table, or a new artifact root outside the existing `storage/logs/test-lanes` contract.
### Deployment Sequencing Note
- No database migration is planned.
- No asset publish step changes.
- The rollout should start with heavy-family inventory and classification rules, then tighten selectors and guard tests, then extend heavy-attribution reporting, and finally validate the affected lanes against the existing baseline and budget seams.

View File

@ -0,0 +1,139 @@
# Quickstart: Filament/Livewire Heavy Suite Segmentation
## Goal
Segment the current heavy Filament, Livewire, surface, discovery, and browser families so Fast Feedback stays lean, Confidence retains meaningful UI trust, Heavy Governance becomes the deliberate home for broad governance scans, and heavy cost is attributable by class and family rather than only by lane total.
## Implementation Order
1. Inventory the current heavy families already visible in `tests/Feature/Filament`, `tests/Feature/Rbac`, `tests/Feature/Baselines`, `tests/Feature/Concerns`, and `tests/Browser`.
2. Finalize the checked-in heavy class catalog: `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, and `browser`.
3. Extend `TestLaneManifest` with the heavy-classification catalog, explicit heavy-family inventory, and budget targets for the first seeded heavy families.
4. Add or refine Pest groups and manifest selectors so the seeded heavy families are technically separable without broad directory churn.
5. Move the broadest Surface-Guard and Discovery-Heavy families out of Fast Feedback and Confidence into explicit `heavy-governance` ownership.
6. Add or expand guard tests so class-to-lane drift fails clearly for Browser, Discovery-Heavy, and broad Surface-Guard families.
7. Extend `TestLaneReport` so heavy-lane output attributes cost by heavy class and named family.
8. Validate Fast Feedback, Confidence, and Heavy Governance against the existing post-Spec 207 baselines and current heavy-lane thresholds.
9. Publish concise author and reviewer guidance explaining how to classify new heavy UI tests.
## Suggested Code Touches
```text
apps/platform/composer.json
apps/platform/tests/Pest.php
apps/platform/tests/Support/TestLaneManifest.php
apps/platform/tests/Support/TestLaneBudget.php
apps/platform/tests/Support/TestLaneReport.php
apps/platform/tests/Feature/Guards/*
apps/platform/tests/Feature/Filament/*
apps/platform/tests/Feature/Rbac/*
apps/platform/tests/Feature/Baselines/*
apps/platform/tests/Feature/Concerns/*
apps/platform/tests/Browser/*
scripts/platform-test-lane
scripts/platform-test-report
```
## Validation Flow
Validate the segmented end-state through the existing checked-in lane wrappers first:
```bash
./scripts/platform-test-lane fast-feedback
./scripts/platform-test-lane confidence
./scripts/platform-test-lane heavy-governance
./scripts/platform-test-lane browser
./scripts/platform-test-lane profiling
./scripts/platform-test-report fast-feedback
./scripts/platform-test-report confidence
./scripts/platform-test-report heavy-governance
./scripts/platform-test-report profiling
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
Keep the implementation loop tight with the most relevant focused suites first:
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament --filter=PolicyResourceAdminSearchParityTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Baselines --filter=BaselineCompareMatrixPageTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Rbac --filter=UiEnforcement
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser
```
## Recorded Baselines
Use the existing Spec 206/207 evidence as the starting comparison point for this rollout.
| Scope | Current reference | Budget / note |
|-------|-------------------|---------------|
| `fast-feedback` | `176.74s` post-Spec 207 comparison baseline | Must stay stable or improve while excluding Discovery-Heavy and broad Surface-Guard families |
| `confidence` | `394.38s` post-Spec 207 comparison baseline | Must stay stable or improve while retaining documented UI-Light and selected UI-Workflow coverage |
| `heavy-governance` | Existing documented heavy-lane threshold `300s`; refresh measured baseline during rollout | Needs updated family attribution after segmentation |
| `browser` | Existing isolated lane with dedicated budget | Remains separate and must not diffuse into other lanes |
## Classification Rules
- `ui-light`: localized component, page, or narrow action tests with limited mounts, limited reflection, and no broad discovery.
- `ui-workflow`: bounded multi-step or multi-surface workflows that still represent real product trust and normally belong in Confidence.
- `surface-guard`: broad action-surface, header-action, navigation-discipline, or relation-manager governance tests whose breadth makes them intentional heavy checks.
- `discovery-heavy`: tests that scan resources, pages, relation managers, global-search behavior, or reflection-heavy registries across a broad surface.
- `browser`: real end-to-end browser interaction and smoke coverage; always separate.
## Seeded Family Examples
Use the manifest catalog as the source of truth. The first checked-in families should stay aligned with these examples:
| Family | Classification | Target lane | Representative hotspots |
|-------|----------------|-------------|--------------------------|
| `backup-set-admin-tenant-parity` | `ui-light` | `confidence` | `tests/Feature/Filament/BackupSetAdminTenantParityTest.php` |
| `baseline-compare-matrix-workflow` | `ui-workflow` | `confidence` | `tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php`, `tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php` |
| `onboarding-wizard-enforcement` | `ui-workflow` | `confidence` | `tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php` |
| `finding-bulk-actions-workflow` | `ui-workflow` | `heavy-governance` | `tests/Feature/Findings/FindingBulkActionsTest.php` |
| `findings-workflow-surfaces` | `ui-workflow` | `heavy-governance` | `tests/Feature/Findings/FindingWorkflowRowActionsTest.php`, `tests/Feature/Findings/FindingWorkflowViewActionsTest.php`, `tests/Feature/Findings/FindingsListFiltersTest.php`, `tests/Feature/Findings/FindingExceptionRenewalTest.php` |
| `drift-bulk-triage-all-matching` | `ui-workflow` | `heavy-governance` | `tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php` |
| `baseline-profile-start-surfaces` | `ui-workflow` | `heavy-governance` | `tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, `tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `tests/Feature/Filament/BaselineActionAuthorizationTest.php` |
| `workspace-settings-slice-management` | `ui-workflow` | `heavy-governance` | `tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php` |
| `workspace-only-admin-surface-independence` | `surface-guard` | `heavy-governance` | `tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php` |
| `action-surface-contract` | `surface-guard` | `heavy-governance` | `tests/Feature/Guards/ActionSurfaceContractTest.php` |
| `backup-items-relation-manager-enforcement` | `surface-guard` | `heavy-governance` | `tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php` |
| `workspace-memberships-relation-manager-enforcement` | `surface-guard` | `heavy-governance` | `tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php` |
| `tenant-review-header-discipline` | `surface-guard` | `heavy-governance` | `tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php` |
| `panel-navigation-segregation` | `surface-guard` | `heavy-governance` | `tests/Feature/Filament/PanelNavigationSegregationTest.php` |
| `ops-ux-governance` | `surface-guard` | `heavy-governance` | `tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php`, `tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`, `tests/Feature/ProviderConnections/CredentialLeakGuardTest.php` |
| `policy-resource-admin-search-parity` | `discovery-heavy` | `heavy-governance` | `tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php` |
| `policy-version-admin-search-parity` | `discovery-heavy` | `heavy-governance` | `tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php` |
| `browser-smoke` | `browser` | `browser` | `tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php` |
Mixed-file fallback remains explicit for `PolicyVersionAdminSearchParityTest.php`, `BaselineCompareMatrixBuilderTest.php`, `BuildsBaselineCompareMatrixFixtures.php`, and `BuildsPortfolioTriageFixtures.php`; reviewers should treat the manifest record as authoritative until those files are further split.
## Reviewer Guidance
1. Default new localized Filament assertions to `ui-light` only when the test stays scoped to one page or action surface and does not enumerate broad registries.
2. Keep a test in `confidence` only if the manifest can justify it as `ui-light` or selected `ui-workflow` with a clear confidence rationale.
3. Move any test that scans resources, global search, relation-manager registries, or broad action inventories into `discovery-heavy` or `surface-guard` even if it still lives under `tests/Feature/Filament`.
4. Treat relation-manager enforcement, header-discipline, navigation-discipline, and action-surface governance as `heavy-governance` by default unless the scope is materially narrowed.
5. When a file mixes confidence-worthy workflow checks and heavier fixture or discovery setup, record the dominant cost driver in the manifest and let the guard output explain the fallback.
6. Keep browser smoke and end-to-end interaction isolated in the `browser` class and lane; no browser file should be justified into Fast Feedback or Confidence.
7. Review heavy attribution reports by family first, then by classification, so lane-total regressions are traced back to a named hotspot family instead of a generic heavy bucket.
## Manual Review Checklist
1. Confirm every seeded heavy family has a checked-in class, current lane, target lane, selectors, and hotspot files.
2. Confirm Fast Feedback contains no Discovery-Heavy families and no broad Surface-Guard families.
3. Confirm Confidence retains only documented `ui-light` families and explicitly selected `ui-workflow` families.
4. Confirm Browser remains isolated by suite, group, and guard tests.
5. Confirm mixed files are either split or explicitly classified by their broadest cost driver.
6. Confirm heavy-lane reporting attributes slowest cost by heavy class and family, not only by lane total.
7. Confirm new drift guards fail with actionable output that names the violating file or family.
8. Confirm author guidance makes it obvious when a new test is too heavy for Fast Feedback or Confidence.
## Exit Criteria
1. The repository has a checked-in heavy classification catalog and seeded heavy family inventory.
2. Fast Feedback and Confidence exclude the families they are no longer supposed to carry.
3. Heavy Governance owns the broad Surface-Guard and Discovery-Heavy families targeted by the rollout.
4. Browser remains a separate class and lane.
5. Drift guards prevent wrong-lane placement for Browser and seeded heavy governance families.
6. Heavy-lane reporting exposes class- and family-level attribution.
7. Fast Feedback and Confidence remain stable or improved against the recorded post-Spec 207 baselines.

View File

@ -0,0 +1,82 @@
# Research: Filament/Livewire Heavy Suite Segmentation
## Decision 1: Reuse the existing lane governance infrastructure instead of adding a new runner layer
- Decision: Spec 208 should extend the current `TestLaneManifest`, `TestLaneBudget`, `TestLaneReport`, Composer lane commands, and repo-root wrapper scripts rather than introducing a second classification or execution framework.
- Rationale: Spec 206 already established canonical lane entry points, artifact generation, budget reporting, and guard tests. Spec 208's problem is semantic classification and drift control inside that existing system, not missing execution plumbing.
- Alternatives considered:
- Create a second heavy-suite manifest or dedicated classification runner: rejected because it would duplicate the existing lane contract and create a parallel maintenance burden.
- Perform only local per-file exclusions: rejected because the problem is repository-wide lane drift, not a single-file exception.
## Decision 2: Use a five-class heavy UI taxonomy tied to cost and purpose
- Decision: The heavy segmentation model should use exactly five classes for the first slice: `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, and `browser`.
- Rationale: The spec needs enough vocabulary to distinguish localized component checks from broad discovery scans and governance-wide surface guards, but not so much taxonomy that authors and reviewers stop applying it consistently.
- Alternatives considered:
- Reuse only the existing lane names: rejected because `fast-feedback`, `confidence`, and `heavy-governance` are execution targets, not sufficient descriptions of test character.
- Create many sub-classes for every Filament test style: rejected because that would overfit current files and violate the proportionality constraint.
## Decision 3: Keep `heavy-governance` as the heavy operational lane instead of introducing a new fifth runtime lane
- Decision: Surface-Guard and Discovery-Heavy families should flow into the existing `heavy-governance` lane, with `browser` remaining separate.
- Rationale: The repository already has a checked-in heavy lane with command wiring, artifacts, budgets, and guard coverage. The missing piece is better segmentation inside that lane, not another operational lane.
- Alternatives considered:
- Introduce a separate `heavy-ui` operational lane: rejected because the current problem can be solved by better family attribution inside the existing heavy lane.
- Merge Browser into heavy governance: rejected because browser is already a distinct cost class with stronger isolation and different runtime semantics.
## Decision 4: Represent heavy-family ownership through a hybrid of manifest selectors, Pest groups, and explicit hotspot inventory
- Decision: The source of truth for heavy families should remain hybrid: manifest selectors for lane execution, Pest groups where group-level drift control helps, and an explicit checked-in family inventory for reviewer visibility.
- Rationale: The current suite already uses all three seams in different ways. A hybrid model gives Spec 208 enough precision to isolate heavy families now without forcing a disruptive directory-first rewrite.
- Alternatives considered:
- Directory-only classification: rejected because mixed files and scattered hotspot tests would remain opaque.
- Group-only classification: rejected because many heavy families are not yet grouped, and relying on groups alone would delay adoption.
- File-list-only classification: rejected because it does not scale as families grow.
## Decision 5: Treat mixed files as a first-class segmentation problem
- Decision: When one file combines multiple cost patterns, the repository should either split the file or classify it by the broadest cost-driving behavior.
- Rationale: Several current families mix localized assertions with discovery or surface-wide checks. Directory folklore cannot resolve those cases reliably, and reviewers need an explicit rule.
- Alternatives considered:
- Let mixed files stay where they are until they become too slow: rejected because the spec is about preventing lane drift before runtime erosion becomes normal.
- Always force a split: rejected because some files may be readable enough if their broadest cost driver is explicit and guarded.
## Decision 6: Seed the heavy-family inventory from the current hotspot clusters rather than attempting a full-suite rewrite
- Decision: The first inventory should focus on the current heavy clusters that already distort lane cost: broad `tests/Feature/Filament` families, action-surface and header-action discipline tests, navigation-discipline tests, RBAC relation-manager and wizard UI-enforcement families, Baseline Compare feature and Filament pages, concern-based fixture builders, and the existing Browser suite.
- Rationale: Exploratory repository analysis found these clusters repeatedly combining multi-mount Livewire tests, relation-manager breadth, wizard step flows, reflection-based discovery, header-action and navigation-discipline checks, broad action-surface validation, and expensive fixture construction.
- Alternatives considered:
- Classify every Feature test in one pass: rejected because it would add excessive churn before the taxonomy is proven.
- Rely only on profiling output without a starting inventory: rejected because the current hotspots are already visible enough to justify an initial catalog.
## Decision 7: Extend drift guards from coarse lane isolation to explicit class-to-lane validation
- Decision: Spec 208 should add or expand guard tests so they validate heavy classification membership, lane compatibility, and wrong-lane drift for `browser`, `surface-guard`, and `discovery-heavy` families.
- Rationale: The repository already has coarse guards for browser isolation and initial heavy-governance placement. Spec 208 needs those guards to become semantic instead of only path-based.
- Alternatives considered:
- Rely on documentation only: rejected because lane drift is a regression problem and must fail automatically.
- Rely only on manual review: rejected because the suite surface is already large enough that inconsistent review would reintroduce drift quickly.
## Decision 8: Attribute heavy cost by class and family in reporting, not just by lane
- Decision: `TestLaneReport` and the family budget contract should be extended so heavy cost can be reported by heavy class and named family, not only by lane total.
- Rationale: Once heavy families move out of faster lanes, maintainers still need to know whether drift is coming from discovery-heavy scans, surface-guard breadth, or workflow-heavy multi-mount tests.
- Alternatives considered:
- Keep lane-only reporting: rejected because lane totals alone cannot explain which heavy family is growing.
- Add only per-file slowest output: rejected because file-level output lacks the semantic grouping reviewers need for governance decisions.
## Decision 9: Preserve Confidence with documented UI-Light and selected UI-Workflow coverage
- Decision: Confidence should explicitly retain localized `ui-light` coverage and a curated subset of `ui-workflow` tests, while Discovery-Heavy and broad Surface-Guard families move to `heavy-governance`.
- Rationale: The spec explicitly rejects hollowing out Confidence. The lane must still carry meaningful UI safety even as the broadest heavy governance families are removed.
- Alternatives considered:
- Move nearly all Filament and Livewire tests to Heavy Governance: rejected because it would turn Confidence into a weak smoke-only lane.
- Leave all current UI-heavy families in Confidence: rejected because that would fail the spec's fast-lane preservation goal.
## Decision 10: Keep the classification catalog repo-local and current-release only
- Decision: The classification catalog should stay inside repository planning and test-governance seams, with no new product persistence, runtime service, or cross-domain application taxonomy.
- Rationale: The problem is local to test-suite architecture and developer feedback loops. Anything broader would violate PROP-001 and import unnecessary maintenance cost.
- Alternatives considered:
- Build a generic testing metadata system for future features: rejected because the current need is narrower and already satisfied by the manifest-plus-guards model.
- Persist test family metadata outside the repo: rejected because the information is build-time governance data, not product truth.

View File

@ -0,0 +1,303 @@
# Feature Specification: Filament/Livewire Heavy Suite Segmentation
**Feature Branch**: `208-heavy-suite-segmentation`
**Created**: 2026-04-16
**Status**: Draft
**Input**: User description: "Spec 208 - Filament/Livewire Heavy Suite Segmentation"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: TenantPilot's growing Filament-, Livewire-, surface-, discovery-, and governance-heavy test families are still mixed too closely with faster feedback paths, so the suite pays heavy UI and discovery cost in lanes that should stay lean.
- **Today's failure**: Broad surface guards, discovery-heavy checks, and multi-mount workflow tests can drift into Fast Feedback or Confidence lanes without a clear classification model, making runtime slower while hiding which families are responsible.
- **User-visible improvement**: Contributors get faster and more predictable standard lanes, maintainers can see heavy UI and discovery families as their own cost class, and reviewers can place new heavy tests correctly without relying on local tribal knowledge.
- **Smallest enterprise-capable version**: Inventory the heavy Filament or Livewire families, define a small classification model, map those classes to existing lanes, make heavy families technically separable, add drift guards, document author guidance, and validate that the affected lanes still provide the intended confidence.
- **Explicit non-goals**: No blanket reduction of Filament or Livewire coverage, no browser-strategy redesign, no wholesale rewrite of every UI test, no product runtime refactor, and no CI-matrix wiring rollout in this spec.
- **Permanent complexity imported**: A repo-level heavy-test classification vocabulary, lane-mapping rules, heavy-family inventory, drift-guard expectations, and explicit heavy-lane budget visibility.
- **Why now**: Spec 206 established lane governance and Spec 207 lowers per-test setup cost; without segmenting the heaviest UI and discovery families next, faster lanes will keep eroding as the platform surface grows.
- **Why not local**: Local file moves or one-off exclusions cannot keep heavy families out of the wrong lane over time. The classification and placement rules must be shared, visible, and enforceable at repository level.
- **Approval class**: Cleanup
- **Red flags triggered**: New classification vocabulary and a new repo-wide taxonomy for heavy tests. Defense: the taxonomy is intentionally narrow, limited to test-lane governance, and does not introduce new product runtime truth, new product persistence, or speculative application abstractions.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: No end-user HTTP routes change. The affected surfaces are repository-level lane manifests, grouping rules, wrapper entry points, profiling outputs, hotspot visibility, and checked-in author or reviewer guidance.
- **Data Ownership**: Workspace-owned test taxonomy, family inventory, lane assignments, drift guards, heavy-lane budget rules, and reporting evidence. No tenant-owned runtime records or end-user product data are introduced.
- **RBAC**: No end-user authorization behavior changes. The affected actors are repository contributors, reviewers, and maintainers who need stable, explicit lane placement for heavy UI and discovery tests.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no new product persistence; only repository-owned classification metadata, checked-in guidance, and runtime evidence for lane validation
- **New abstraction?**: yes, but limited to a repository-level heavy-suite classification and lane-mapping model
- **New enum/state/reason family?**: no product runtime state is added; the new categories are test-governance classes only
- **New cross-domain UI framework/taxonomy?**: yes, but narrowly scoped to classifying heavy test families by cost and purpose so lane placement remains reviewable and enforceable
- **Current operator problem**: Contributors and maintainers cannot keep fast lanes lean when broad Filament or Livewire discovery and surface-governance families look similar to ordinary UI tests and drift into the wrong run path.
- **Existing structure is insufficient because**: Directory names and current lane assignments do not reliably communicate discovery breadth, mount count, reflection cost, or surface-wide governance intent, so the real cost class is often only visible after runtime has already degraded.
- **Narrowest correct implementation**: Add a small classification model, map it to existing lanes, inventory the heaviest current families, make those families technically separable, add drift guards, and publish concise author guidance.
- **Ownership cost**: The team must maintain class definitions, lane-mapping rules, heavy-family inventory, drift guards, and heavy-budget visibility as the suite evolves.
- **Alternative intentionally rejected**: Purely directory-based moves or ad-hoc exclusions without a shared semantic model, because those approaches hide cost instead of making it governable.
- **Release truth**: Current-release repository truth that protects the lane model already introduced in Spec 206 and strengthened by Spec 207.
## Problem Statement
Even with slimmer fixtures, the suite stays expensive if the heaviest UI and discovery families remain organizationally mixed with ordinary feedback paths.
The structural issues are now clear:
- Filament and Livewire tests do not all belong to the same cost class.
- Discovery-heavy and surface-wide guard tests grow with every new resource, page, relation manager, or system surface.
- Broad UI or surface checks can compete directly with the fast authoring loop when they land in the wrong lane.
- Lane placement is unstable if cost and purpose are not classified explicitly.
- Some tests only reveal their real heaviness at runtime because they combine discovery, reflection, multiple mounts, and broad assertion sets in one file.
- The most expensive families need their own budgets and visibility so runtime drift can be attributed accurately.
Without explicit segmentation, the Fast Feedback and Confidence lanes created by Spec 206 will gradually absorb more broad UI and discovery cost, even after per-test setup has been slimmed by Spec 207.
## Dependencies
- Depends on Spec 206 - Test Suite Governance & Performance Foundation for the lane vocabulary, wrappers, baseline visibility, and budget discipline.
- Recommended after Spec 207 - Shared Test Fixture Slimming so per-test setup cost is reduced before the heaviest families are separated more sharply.
- Blocks clean lane-based separation of expensive UI-, surface-, and discovery-heavy families.
- Does not block ongoing feature delivery as long as newly added heavy tests are classified and placed lane-conform from the start.
## Goals
- Identify and classify heavy Filament, Livewire, surface, and discovery test families clearly.
- Protect Fast Feedback from deliberate heavy UI and discovery cost.
- Keep the Confidence lane broad enough to remain trusted while excluding surface-wide heavy governance families.
- Establish Heavy Governance as the explicit home for surface-guard and discovery-heavy families.
- Prevent new heavy tests from drifting silently into the wrong lane.
- Preserve valuable Filament and Livewire safety while making its operating cost visible and governable.
- Make future UI test growth scale through explicit class and lane rules rather than informal directory folklore.
## Non-Goals
- Reducing legitimate Filament or Livewire coverage just to improve runtime headline numbers.
- Redefining the Browser lane or folding browser behavior into this spec.
- Replacing the fixture-cost work from Spec 207.
- Rewriting every existing UI test into a new structure in one pass.
- Performing CI-matrix or enforcement wiring that belongs in a later spec.
- Changing Filament runtime semantics, application IA, or product behavior outside repository test organization and guidance.
## Assumptions
- The lane model, wrappers, and baseline discipline from Spec 206 already exist or can be regenerated before this rollout is validated.
- Fixture slimming from Spec 207 lowers per-test setup cost but does not by itself solve heavy-family placement.
- Browser remains a separate cost class and is not merged into the heavy Filament or Livewire taxonomy.
- Not every class needs its own dedicated directory, as long as its lane placement is explicit, technically separable, and reviewable.
- Some borderline UI workflow tests may remain in Confidence if their scope is localized and their cost stays within documented lane budgets.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Classify Existing Heavy UI Families (Priority: P1)
As a maintainer, I want the current heavy Filament, Livewire, surface, and discovery families inventoried and classified so the repository can stop treating them as a single undifferentiated UI cost bucket.
**Why this priority**: The rollout cannot be reliable until the existing heavy families are visible, named, and assigned a clear cost class.
**Independent Test**: Review the inventory produced by the rollout and confirm that each targeted heavy family has a named class, a purpose, a current lane, a target lane, and identifiable hotspot files.
**Acceptance Scenarios**:
1. **Given** a currently known heavy Filament or Livewire family, **When** the inventory is reviewed, **Then** its cost class, purpose, current lane, target lane, and hotspot files are documented.
2. **Given** a family performs broad discovery, reflection, or surface-wide validation, **When** it is classified, **Then** it is not treated as ordinary UI-Light coverage.
3. **Given** a family mixes UI interaction and governance-wide inspection, **When** it is reviewed, **Then** the broadest cost-driving behavior determines its class unless the family is split intentionally.
---
### User Story 2 - Keep Fast And Confidence Lanes Honest (Priority: P1)
As a contributor, I want Fast Feedback and Confidence to exclude the heaviest surface-wide and discovery-wide families so the normal authoring loop stays fast without hiding where broader governance checks now run.
**Why this priority**: Lane shaping only matters if the most commonly used runs stop paying for deliberately heavy families.
**Independent Test**: Run the affected lanes after segmentation and verify that Fast Feedback excludes Discovery-Heavy and broad Surface-Guard families, while Confidence retains only documented UI-Light and selected UI-Workflow coverage.
**Acceptance Scenarios**:
1. **Given** a family is classified as Discovery-Heavy or a broad Surface-Guard, **When** Fast Feedback runs, **Then** that family is excluded.
2. **Given** a UI-Light or selected UI-Workflow family is explicitly approved for Confidence, **When** Confidence runs, **Then** it remains present without pulling in undocumented heavy governance families.
3. **Given** a maintainer compares lane manifests after segmentation, **When** they inspect Fast Feedback and Confidence, **Then** the absence or inclusion of heavy UI families is explainable from written rules instead of local habit.
---
### User Story 3 - Catch Lane Drift During Review (Priority: P2)
As a reviewer, I want clear author rules and drift guards so a newly added heavy UI test cannot silently land in the wrong lane.
**Why this priority**: The segmentation work decays quickly if new tests can bypass it without being noticed.
**Independent Test**: Add or move a representative heavy test to the wrong lane and confirm that the repository's drift guard or validation path fails clearly enough for a reviewer to correct placement.
**Acceptance Scenarios**:
1. **Given** a new test performs broad resource discovery or surface-wide governance checks, **When** it is placed in Fast Feedback or ordinary Confidence by mistake, **Then** drift validation flags the mismatch.
2. **Given** a reviewer inspects a new heavy UI test, **When** they read the repository guidance, **Then** they can determine whether the test is UI-Light, UI-Workflow, Surface-Guard, Discovery-Heavy, or Browser.
3. **Given** a test belongs to the Browser class, **When** it is assigned to a non-browser lane, **Then** the repository rejects the placement.
---
### User Story 4 - Observe Heavy Budgets Separately (Priority: P2)
As a maintainer, I want the Heavy Governance lane and its heaviest families measured separately so runtime drift is visible without distorting the health signal of faster lanes.
**Why this priority**: Once heavy families are separated, the team still needs to see whether they are growing responsibly or becoming the next uncontrolled hotspot cluster.
**Independent Test**: Generate the heavy-lane reporting view after segmentation and confirm that the top hotspots are attributable to named heavy families or classes instead of appearing as undifferentiated suite slowdown.
**Acceptance Scenarios**:
1. **Given** the Heavy Governance lane completes, **When** its reporting output is reviewed, **Then** the top hotspots are tied to named heavy families or classes.
2. **Given** heavy-family runtime grows over time, **When** maintainers inspect the reporting signal, **Then** they can see whether the drift belongs to discovery-heavy, surface-guard, or workflow-heavy families.
### Edge Cases
- A single file mixes a localized component assertion with broad discovery or reflection, making directory-only classification misleading.
- A discovery-heavy family sits in a generic Feature location and looks cheap until it scans resources or relation managers at runtime.
- A previously narrow UI test becomes heavy after adding multiple mounts, wide action-surface assertions, or broad reflection checks.
- A borderline workflow test is too broad for Fast Feedback but still narrow enough for Confidence if its scope remains local and its cost stays within the documented budget.
- Browser-like interaction depth must not be relabeled as heavy Filament or Livewire merely to avoid Browser-lane isolation.
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature changes no end-user routes, no Microsoft Graph behavior, no authorization plane, no queued operation semantics, and no operator-facing product surface. It does introduce a repository-level taxonomy for heavy UI and discovery tests, so the class model, lane mapping, drift guards, and validation evidence must remain explicit, reviewable, and measurable.
**Constitution alignment (PROP-001 / ABSTR-001 / BLOAT-001 / TEST-TRUTH-001):** The new structure is limited to repository test governance. It is justified because current lane drift is caused by real cost differences between heavy and ordinary UI families, and a narrower local move-only approach cannot keep those families out of the wrong lanes over time. The classification must stay small, cost-driven, and directly tied to business-useful confidence rather than creating an elaborate semantic framework.
### Functional Requirements
- **FR-001 Heavy Classification Model**: The repository MUST define a documented classification model for heavy UI-related test families with at least these classes: UI-Light, UI-Workflow, Surface-Guard, Discovery-Heavy, and Browser.
- **FR-002 Class Meaning**: Each class MUST have a written purpose, typical cost profile, characteristic behaviors, and boundary rules that explain why a test belongs there.
- **FR-003 Cost-And-Purpose Placement**: Test classification MUST be determined by cost and purpose, including mount breadth, discovery breadth, reflection passes, assertion density, and surface coverage, rather than by directory name alone.
- **FR-004 Lane Mapping Rules**: The repository MUST define binding lane-mapping rules stating which classes may appear in Fast Feedback, Confidence, Heavy Governance, and Browser.
- **FR-005 Fast Feedback Protection**: Fast Feedback MUST exclude Discovery-Heavy families and broad Surface-Guard families. Only explicitly approved, low-cost UI-Light cases may remain there.
- **FR-006 Confidence Preservation**: Confidence MUST retain documented UI-Light coverage and selected UI-Workflow coverage, while excluding undocumented discovery-wide or surface-wide heavy governance families. The repository MUST explain which UI safety remains in Confidence and why the exclusions are safe.
- **FR-007 Heavy Governance Isolation**: Heavy Governance MUST be the explicit home for Surface-Guard, Discovery-Heavy, and other broad Filament or Livewire contract families whose cost or breadth makes them unsuitable for ordinary authoring loops.
- **FR-008 Browser Isolation**: Browser MUST remain an independent class and lane. Browser tests MUST NOT diffuse into Fast Feedback, Confidence, or Heavy Governance.
- **FR-009 Heavy Family Inventory**: The rollout MUST inventory the heaviest Filament, Livewire, surface, and discovery families touched by current lanes, documenting for each family its purpose, class, current lane, target lane, and hotspot files.
- **FR-010 Technical Separability**: Heavy families MUST be technically separable through an explicit combination of grouping, manifest assignment, namespace or directory convention, wrapper metadata, or equivalent checked-in mechanism. Lane placement MUST NOT depend on unwritten team knowledge.
- **FR-011 Surface Family Rationalization**: The heaviest action-surface, header-action, navigation-discipline, relation-manager, wizard, and discovery-wide governance families MUST be reviewed and intentionally placed in the correct class and lane.
- **FR-012 Drift Guards**: The repository MUST include guard logic, validation tests, or an equivalent checked-in control that detects when Browser, Discovery-Heavy, or broad Surface-Guard families are assigned to the wrong lane.
- **FR-013 Mixed-Family Handling**: When a file combines multiple behaviors with different cost classes, the repository MUST either split the file or classify it by its broadest cost-driving behavior and document that choice.
- **FR-014 Heavy Budget Visibility**: Heavy Governance and other heavy UI family outputs MUST expose a lane budget, top hotspots, and named family or class attribution so runtime drift is visible over time.
- **FR-015 Author And Reviewer Guidance**: Contributors and reviewers MUST have concise, binding guidance that explains when a test is UI-Light, UI-Workflow, Surface-Guard, Discovery-Heavy, or Browser, and when a test is too heavy for Fast Feedback or Confidence.
- **FR-016 Validation Evidence**: After segmentation, the affected lanes MUST be validated successfully, and the validation evidence MUST show that faster lanes remain protected while heavy governance coverage is still runnable and observable.
### Non-Functional Requirements
- **NFR-001 Fast-Lane Integrity**: Fast Feedback must remain stable or improved against its post-Spec 207 baseline and must measure lower wall-clock duration than Confidence and Heavy Governance in wrapper validation after segmentation.
- **NFR-002 Review Clarity**: A reviewer must be able to determine the intended class and lane of a new heavy UI test from checked-in guidance and test-local signals without case-by-case reinvention.
- **NFR-003 Observability**: Heavy-family runtime drift must be attributable to named classes or families rather than surfacing only as general suite slowdown.
- **NFR-004 Scalability Readiness**: As the platform gains more resources, pages, and relation managers, growth in broad discovery or surface-governance coverage must scale primarily in Heavy Governance rather than silently inflating faster lanes.
## Work Packages
### Work Package A - Heavy Family Inventory
- Catalogue the current heavy Filament, Livewire, surface, and discovery families.
- Record each family's purpose, cost class, current lane, target lane, and hotspot files.
- Identify the most expensive broad-surface and discovery-heavy families that currently distort faster lanes.
- Distinguish localized workflow coverage from governance-wide inspection families before any mechanical moves begin.
### Work Package B - Classification And Lane Rules
- Finalize the UI-Light, UI-Workflow, Surface-Guard, Discovery-Heavy, and Browser classes.
- Define the lane-mapping rules for Fast Feedback, Confidence, Heavy Governance, and Browser.
- Document boundary rules for borderline cases, especially mixed workflow and discovery behavior.
- Publish concise reviewer rules so lane placement decisions stay consistent.
### Work Package C - Mechanical Segmentation
- Adjust groups, manifests, wrapper metadata, namespaces, directories, or other checked-in selectors so heavy families are technically separable.
- Move or reassign the broadest heavy families into their correct lane.
- Ensure ordinary faster lanes cannot inherit those families accidentally.
- Keep the resulting structure visible enough that reviewers can understand placement intent immediately.
### Work Package D - Drift Guards
- Add validation that Browser families stay browser-only.
- Add validation that Discovery-Heavy and broad Surface-Guard families do not drift into Fast Feedback or ordinary Confidence.
- Protect against newly added heavy families being treated as ordinary UI tests without a classification decision.
- Ensure mixed or borderline families are either split or explicitly justified.
### Work Package E - Lane Validation
- Validate Fast Feedback after heavy-family removal.
- Validate Confidence after the intended UI-Light and UI-Workflow coverage remains.
- Validate Heavy Governance as a runnable lane for deliberate surface and discovery cost.
- Compare the resulting lane behavior against the post-Spec 207 baseline so the benefit and the retained confidence are both visible.
## Deliverables
- An inventory of the heavy Filament, Livewire, surface, and discovery families touched by this rollout.
- A documented heavy-test classification model.
- Updated lane mappings and checked-in technical separation for the targeted heavy families.
- Drift guards that reject wrong-lane placement for Browser and heavy governance families.
- Concise author and reviewer guidance for new heavy UI tests.
- Validation evidence for Fast Feedback, Confidence, and Heavy Governance after segmentation.
- Updated heavy-lane budget and hotspot visibility.
## Risks
### False Confidence Through Over-Segmentation
If too much meaningful UI safety leaves Confidence, the faster lane mix may look healthy while real product trust declines.
### Taxonomy Complexity
If the class model grows too many exceptions or subclasses, contributors may stop using it consistently.
### Mechanical Moves Without Semantic Clarity
If files are merely moved between groups or directories without a clear class model, the repository will hide cost rather than govern it.
### Reviewer Inconsistency
If reviewers do not apply the new class and lane rules consistently, heavy-family drift will return quickly.
### Hidden Cost Inside Ordinary Files
Some heavy behavior is only visible through runtime breadth, so directory rules alone can miss expensive mixed files unless drift guards are strong enough.
## Rollout Guidance
- Inventory the heavy families before changing structure.
- Finalize the class model and lane rules before large mechanical moves.
- Move the broadest and most obviously misplaced heavy families first.
- Add drift guards before the rollout is considered complete.
- Validate faster lanes and Heavy Governance separately after segmentation.
- Document author and reviewer guidance only after the class model and lane rules are stable.
- Avoid broad mechanical relocation before the semantic boundaries are explicit.
## Design Rules
- **Heavy must be explicit**: A broad UI, surface, or discovery family must never look like an ordinary test by accident.
- **Fast feedback stays fast**: Fast Feedback must not inherit deliberate governance-wide Filament or Livewire cost.
- **Confidence keeps real trust**: Confidence must still carry meaningful UI-Light and selected UI-Workflow safety.
- **Discovery is governance**: Broad discovery and reflection families are governance checks, not convenience coverage.
- **Browser is always separate**: Browser remains its own class and lane.
- **Classification beats directory folklore**: The real test character decides placement, not the folder name alone.
- **Reviewer intent must be immediate**: A reviewer must be able to see why a heavy test belongs where it does.
## Key Entities *(include if feature involves data)*
- **Heavy Test Class**: A repository-defined cost and purpose category used to describe a UI-related test family's expected behavior and lane fit.
- **Heavy Test Family**: A named cluster of related tests, usually sharing a surface, workflow, discovery pattern, or governance concern.
- **Lane Mapping Rule**: A checked-in rule that defines which test classes may or may not appear in each operational lane.
- **Drift Guard**: A validation path that detects when a test family is assigned to a lane that conflicts with its class.
- **Heavy Budget View**: The reporting signal that shows the runtime budget, top hotspots, and attribution for heavy families and their lane.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Every heavy Filament, Livewire, surface, or discovery family targeted by this rollout is classified, assigned a target lane, and documented with no unclassified exceptions remaining in the touched scope.
- **SC-002**: Fast Feedback contains zero Discovery-Heavy families and zero broad Surface-Guard families after segmentation.
- **SC-003**: Confidence contains only documented UI-Light families and explicitly selected UI-Workflow families, with zero undocumented heavy-governance exceptions.
- **SC-004**: Heavy Governance reporting exposes at least the top 10 runtime hotspots with named family or class attribution.
- **SC-005**: Wrong-lane placement for Browser, Discovery-Heavy, or broad Surface-Guard families is detected by a checked-in validation path before those tests become accepted lane members.
- **SC-006**: Fast Feedback and Confidence are stable or improved against their post-Spec 207 baselines after segmentation, Fast Feedback remains lower wall-clock than Confidence and Heavy Governance in wrapper validation, and the documented UI safety coverage assigned to each lane remains intact.
- **SC-007**: A reviewer can classify a newly added heavy UI-related test using checked-in guidance alone, without relying on undocumented local knowledge.

View File

@ -0,0 +1,181 @@
# Tasks: Filament/Livewire Heavy Suite Segmentation
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/208-heavy-suite-segmentation/`
**Prerequisites**: `plan.md` (required), `spec.md` (required), `research.md`, `data-model.md`, `contracts/`, `quickstart.md`
**Tests**: Required. This feature changes repository test-governance behavior, so each user story includes Pest guard or contract coverage and focused validation runs through Sail.
**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently.
## Phase 1: Setup (Shared Context)
**Purpose**: Confirm the existing lane seams, guard anchors, and hotspot files that Spec 208 will segment.
- [X] T001 Audit lane command and artifact entry points in `apps/platform/composer.json`, `scripts/platform-test-lane`, and `scripts/platform-test-report`
- [X] T002 [P] Review the current manifest and shared guard anchors in `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php`, and `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php`
- [X] T003 [P] Review the seeded hotspot clusters in `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php`, `apps/platform/tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php`, `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`, `apps/platform/tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php`, `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Add the shared manifest and artifact seams that all user stories rely on.
**Critical**: No user story work should begin until these tasks are complete.
- [X] T004 Extend `apps/platform/tests/Support/TestLaneManifest.php` with neutral accessors for classification catalogs, family inventory, lane placement rules, and mixed-file resolution to back the logical read operations in `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T005 [P] Update `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php` to accept the expanded manifest metadata shape without locking story-specific families yet
- [X] T006 [P] Update `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` to reserve class and family attribution keys under `apps/platform/storage/logs/test-lanes/`
**Checkpoint**: Shared manifest and artifact seams are ready for story-specific implementation.
---
## Phase 3: User Story 1 - Classify Existing Heavy UI Families (Priority: P1)
**Goal**: Seed a checked-in heavy-family catalog that names the first heavy Filament, Livewire, surface, discovery, and browser families with explicit purpose and lane intent.
**Independent Test**: Review the manifest inventory and guard coverage to confirm each seeded family has a class, purpose, current lane, target lane, selectors, hotspot files, and validation status.
### Tests for User Story 1
- [X] T007 [P] [US1] Expand `apps/platform/tests/Feature/Guards/TestTaxonomyPlacementGuardTest.php` to assert the five-class catalog, allowed lanes, and forbidden lane semantics from `specs/208-heavy-suite-segmentation/contracts/heavy-test-classification.schema.json`
- [X] T008 [P] [US1] Expand `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php` to assert every seeded heavy family in `apps/platform/tests/Support/TestLaneManifest.php` exposes purpose, current lane, target lane, selectors, hotspot files, validation status, and confidence rationale whenever the target lane is `confidence`
### Implementation for User Story 1
- [X] T009 [US1] Populate the `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, and `browser` catalog plus lane placement rules in `apps/platform/tests/Support/TestLaneManifest.php` and expose the classification catalog through the logical read surface defined in `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T010 [US1] Seed the first heavy family inventory in `apps/platform/tests/Support/TestLaneManifest.php` using `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php`, `apps/platform/tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php`, `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`, `apps/platform/tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php`, `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php`, and `apps/platform/tests/Browser/Spec190BaselineCompareMatrixSmokeTest.php`, and expose the family inventory through the logical read surface defined in `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T011 [US1] Record mixed-file resolution and hotspot cost signals in `apps/platform/tests/Support/TestLaneManifest.php` for `apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php`, `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`, and `apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php`, and keep those records consistent with `specs/208-heavy-suite-segmentation/contracts/heavy-test-classification.schema.json` and `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T012 [US1] Align the seeded family inventory examples in `specs/208-heavy-suite-segmentation/quickstart.md` with the canonical catalog stored in `apps/platform/tests/Support/TestLaneManifest.php` without publishing final reviewer guidance yet
**Checkpoint**: User Story 1 is complete when the seeded heavy-family inventory is reviewable and contract-tested without changing lane membership yet.
---
## Phase 4: User Story 2 - Keep Fast And Confidence Lanes Honest (Priority: P1)
**Goal**: Move discovery-heavy and broad surface-guard families out of Fast Feedback and ordinary Confidence while preserving documented `ui-light` and selected `ui-workflow` trust.
**Independent Test**: Run the lane contract guards and wrapper flows to verify Fast Feedback excludes discovery-heavy and broad surface-guard families, Confidence retains only the documented light or workflow families, and Heavy Governance receives the moved families.
### Tests for User Story 2
- [X] T013 [P] [US2] Expand `apps/platform/tests/Feature/Guards/FastFeedbackLaneContractTest.php` and `apps/platform/tests/Feature/Guards/FastFeedbackLaneExclusionTest.php` to assert discovery-heavy and broad surface-guard families never run in Fast Feedback
- [X] T014 [P] [US2] Expand `apps/platform/tests/Feature/Guards/ConfidenceLaneContractTest.php` and `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` to assert only documented `ui-light` and selected `ui-workflow` families with explicit confidence rationale remain in Confidence while moved families land in Heavy Governance
### Implementation for User Story 2
- [X] T015 [US2] Rework Fast Feedback, Confidence, and Heavy Governance membership rules in `apps/platform/tests/Support/TestLaneManifest.php` to consume the seeded family inventory instead of scattered file exclusions
- [X] T016 [US2] Add granular `ui-light`, `ui-workflow`, `surface-guard`, `discovery-heavy`, and `browser` grouping rules in `apps/platform/tests/Pest.php` for the seeded hotspot files
- [X] T017 [P] [US2] Reclassify discovery-heavy Filament parity hotspots in `apps/platform/tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php` and `apps/platform/tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php`
- [X] T018 [P] [US2] Reclassify workflow and matrix hotspots in `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php` and `apps/platform/tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php`
- [X] T019 [P] [US2] Reclassify fixture-amplifying concern hotspots in `apps/platform/tests/Feature/Concerns/BuildsBaselineCompareMatrixFixtures.php` and `apps/platform/tests/Feature/Concerns/BuildsPortfolioTriageFixtures.php`
- [X] T020 [P] [US2] Reclassify broad relation-manager, RBAC, and wizard hotspots in `apps/platform/tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php`, `apps/platform/tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php`, and `apps/platform/tests/Feature/Rbac/OnboardingWizardUiEnforcementTest.php`
- [X] T021 [US2] Reclassify header-action and navigation-discipline hotspots in `apps/platform/tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php` and `apps/platform/tests/Feature/Filament/PanelNavigationSegregationTest.php`, then reconcile lane entry point metadata in `apps/platform/composer.json` and `scripts/platform-test-lane` with the segmented family selectors from `apps/platform/tests/Support/TestLaneManifest.php`
**Checkpoint**: User Story 2 is complete when lane membership is explainable from cataloged family rules and faster lanes no longer carry the prohibited heavy families.
---
## Phase 5: User Story 3 - Catch Lane Drift During Review (Priority: P2)
**Goal**: Make wrong-lane placement fail clearly for Browser, Discovery-Heavy, and broad Surface-Guard families, with contributor guidance that keeps classification decisions reviewable.
**Independent Test**: Introduce a representative wrong-lane placement locally and confirm the guard suite fails with actionable output naming the violating class, family, or file.
### Tests for User Story 3
- [X] T022 [P] [US3] Expand `apps/platform/tests/Feature/Guards/BrowserLaneIsolationTest.php` and `apps/platform/tests/Feature/Guards/TestTaxonomyPlacementGuardTest.php` to fail on browser misplacement and invalid class-to-lane mappings
- [X] T023 [P] [US3] Expand `apps/platform/tests/Feature/Guards/ConfidenceLaneContractTest.php` and `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` to cover mixed-file fallback and wrong-lane regression cases
### Implementation for User Story 3
- [X] T024 [US3] Implement manifest-side lane placement validation and mixed-file resolution helpers in `apps/platform/tests/Support/TestLaneManifest.php` consistent with `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T025 [US3] Add failure-friendly family and class lookup output for guard consumers in `apps/platform/tests/Support/TestLaneManifest.php` and `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php`
- [X] T026 [US3] Publish contributor and reviewer classification guidance in `specs/208-heavy-suite-segmentation/quickstart.md` and keep the canonical reviewer signals in `apps/platform/tests/Support/TestLaneManifest.php`
**Checkpoint**: User Story 3 is complete when wrong-lane drift fails automatically and reviewers can classify new heavy tests from checked-in guidance alone.
---
## Phase 6: User Story 4 - Observe Heavy Budgets Separately (Priority: P2)
**Goal**: Attribute Heavy Governance cost by lane, class, and family so hotspot growth is visible without distorting the signal of faster lanes.
**Independent Test**: Generate heavy-governance report artifacts and confirm they expose the top hotspots, class attribution, family attribution, and budget evaluations under the existing artifact root.
### Tests for User Story 4
- [X] T027 [P] [US4] Expand `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` and `apps/platform/tests/Feature/Guards/ProfileLaneContractTest.php` to assert heavy reports emit the canonical top 10 runtime hotspot view ordered by slowest entries plus class attribution, family attribution, and budget evaluation payloads
- [X] T028 [P] [US4] Expand `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php` and `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` to assert lane-, class-, and family-level budget targets for Heavy Governance
### Implementation for User Story 4
- [X] T029 [US4] Extend `apps/platform/tests/Support/TestLaneBudget.php` with lane, class, and family budget targets plus baseline sources, enforcement levels, and lifecycle states for the seeded heavy families
- [X] T030 [US4] Extend `apps/platform/tests/Support/TestLaneReport.php` to emit the canonical top 10 runtime hotspot view ordered by slowest entries, classification attribution, family attribution, budget evaluations, and artifact paths under `apps/platform/storage/logs/test-lanes/` consistent with `specs/208-heavy-suite-segmentation/contracts/heavy-suite-segmentation.logical.openapi.yaml`
- [X] T031 [US4] Update `scripts/platform-test-report` and `apps/platform/composer.json` to surface the heavy attribution report contract emitted by `apps/platform/tests/Support/TestLaneReport.php` and preserve reviewable top 10 runtime hotspot output for Heavy Governance
**Checkpoint**: User Story 4 is complete when Heavy Governance artifacts explain slowdowns by class and family, not only by lane total.
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Validate the end-to-end rollout, format the code, and remove superseded ad-hoc lane logic.
- [X] T032 Run focused Pest coverage for `apps/platform/tests/Feature/Guards/`, `apps/platform/tests/Feature/Filament/`, `apps/platform/tests/Feature/Baselines/`, and `apps/platform/tests/Feature/Rbac/` with `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards tests/Feature/Filament tests/Feature/Baselines tests/Feature/Rbac`
- [X] T033 Run lane wrapper validation with `scripts/platform-test-lane` and `scripts/platform-test-report`, compare Fast Feedback and Confidence against the post-Spec 207 baselines, confirm Fast Feedback measures lower wall-clock than Confidence and Heavy Governance, record Heavy Governance threshold or refreshed baseline evidence, and inspect emitted artifacts under `apps/platform/storage/logs/test-lanes/`
- [X] T034 Run formatting for `apps/platform/` with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- [X] T035 Remove stale ad-hoc lane comments or exclusions in `apps/platform/tests/Pest.php`, `apps/platform/tests/Support/TestLaneManifest.php`, and `apps/platform/tests/Support/TestLaneBudget.php` once the segmented catalog is authoritative
---
## Dependencies
- Phase 1 must complete before Phase 2.
- Phase 2 must complete before any user story work begins.
- User Story 1 is the MVP and must complete before User Story 2, User Story 3, or User Story 4.
- User Story 2 depends on User Story 1 because lane membership must consume the seeded family inventory.
- User Story 3 depends on User Story 1 and User Story 2 because drift validation relies on the final catalog and lane mappings.
- User Story 4 depends on User Story 1 and User Story 2 because report attribution relies on the seeded family inventory and segmented lane ownership.
- Phase 7 depends on all user stories.
## Parallel Execution Examples
### User Story 1
- Run T007 and T008 in parallel to lock the schema and inventory guard coverage.
- After T009 lands, T010 and T011 can be split between seeded family inventory work and mixed-file resolution work.
### User Story 2
- Run T013 and T014 in parallel because they cover different lane guard files.
- After T015 and T016 land, T017, T018, T019, and T020 can run in parallel across Filament, Baselines, Concerns, and RBAC hotspots.
### User Story 3
- Run T022 and T023 in parallel because they touch separate guard surfaces.
- T024 should land before T025 and T026 so the guidance and failure output describe the actual validation helpers.
### User Story 4
- Run T027 and T028 in parallel because artifact-contract and budget-guard coverage live in different test files.
- After T029 lands, T030 and T031 can be split between report payload generation and wrapper exposure work.
## Implementation Strategy
### MVP First
- Deliver User Story 1 first to establish the canonical heavy-family catalog, seeded inventory, and reviewable hotspot metadata.
- Deliver User Story 2 next to produce the first operational benefit by keeping Fast Feedback and Confidence honest.
### Incremental Delivery
- Add User Story 3 after lane segmentation so drift protection describes the real final placement rules instead of a temporary draft.
- Add User Story 4 after the lane moves settle so the attribution model reflects stable family ownership and budget targets.
### Validation Sequence
- Use focused guard suites first, then targeted hotspot files, then the lane wrappers, and only then refresh formatting and stale exclusion cleanup.
- Treat `apps/platform/storage/logs/test-lanes/` as the canonical artifact root throughout validation and report review.

View File

@ -0,0 +1,38 @@
# Specification Quality Checklist: Heavy Governance Lane Cost Reduction
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-17
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation run: 2026-04-17
- No template placeholders or [NEEDS CLARIFICATION] markers remain.
- The spec stays repo-governance-focused: it describes lane behavior, hotspot evidence, and budget outcomes without prescribing language-, framework-, or API-level implementation.
- The spec keeps Heavy Governance honest by requiring explicit budget recovery or explicit recalibration, rather than silent lane reshuffling.
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`

View File

@ -0,0 +1,509 @@
openapi: 3.1.0
info:
title: Heavy Governance Cost Recovery Logical Contract
version: 1.0.0
summary: Logical contract for inspecting heavy-governance hotspots, recording slimming decisions, and publishing the final budget outcome.
description: |
This is a logical contract for repository tooling, tests, and planning artifacts.
It does not imply a new runtime HTTP service. It documents the expected
semantics of hotspot inventory, family decomposition, budget normalization,
and explicit recovery or recalibration outcomes so the existing heavy-lane
seams remain consistent as Spec 209 is implemented.
x-logical-contract: true
servers:
- url: https://tenantatlas.local/logical
paths:
/heavy-governance/budget-contract:
get:
summary: Read the current heavy-governance budget contract.
operationId: getHeavyGovernanceBudgetContract
responses:
'200':
description: Current heavy-governance budget contract.
content:
application/json:
schema:
$ref: '#/components/schemas/HeavyGovernanceBudgetContract'
/heavy-governance/hotspots:
get:
summary: Read the current heavy-governance hotspot inventory.
operationId: listHeavyGovernanceHotspots
responses:
'200':
description: Current hotspot inventory.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- hotspots
properties:
hotspots:
type: array
minItems: 1
items:
$ref: '#/components/schemas/HeavyGovernanceHotspot'
/heavy-governance/hotspots/{familyId}/decomposition:
get:
summary: Read the decomposition record for a hotspot family.
operationId: getHeavyGovernanceFamilyDecomposition
parameters:
- name: familyId
in: path
required: true
schema:
type: string
responses:
'200':
description: Current decomposition record for the family.
content:
application/json:
schema:
$ref: '#/components/schemas/FamilyCostDecomposition'
put:
summary: Record or update the decomposition record for a hotspot family.
operationId: putHeavyGovernanceFamilyDecomposition
parameters:
- name: familyId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/FamilyCostDecomposition'
responses:
'200':
description: Decomposition record stored logically.
content:
application/json:
schema:
$ref: '#/components/schemas/FamilyCostDecomposition'
/heavy-governance/slimming-decisions:
get:
summary: Read the current slimming decisions for targeted hotspot families.
operationId: listHeavyGovernanceSlimmingDecisions
responses:
'200':
description: Current slimming decisions.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- decisions
properties:
decisions:
type: array
items:
$ref: '#/components/schemas/HeavyFamilySlimmingDecision'
/heavy-governance/budget-snapshots:
get:
summary: Read the recorded heavy-governance budget snapshots.
operationId: listHeavyGovernanceBudgetSnapshots
responses:
'200':
description: Current before-and-after snapshots.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- snapshots
properties:
snapshots:
type: array
items:
$ref: '#/components/schemas/BudgetRecoverySnapshot'
/heavy-governance/budget-outcome/latest:
get:
summary: Read the latest explicit heavy-governance budget outcome.
operationId: getLatestHeavyGovernanceBudgetOutcome
responses:
'200':
description: Latest heavy-governance budget outcome.
content:
application/json:
schema:
$ref: '#/components/schemas/BudgetOutcomeRecord'
/heavy-governance/author-guidance:
get:
summary: Read the reviewer and author rules for future heavy-governance tests.
operationId: listHeavyGovernanceAuthorGuidance
responses:
'200':
description: Current author guidance rules.
content:
application/json:
schema:
type: object
additionalProperties: false
required:
- rules
properties:
rules:
type: array
minItems: 4
items:
$ref: '#/components/schemas/HeavyAuthorGuidanceRule'
components:
schemas:
ClassificationId:
type: string
enum:
- ui-workflow
- surface-guard
- discovery-heavy
CostDriverCategory:
type: string
enum:
- overbroad
- redundant
- discovery-heavy
- workflow-heavy
- surface-heavy
- helper-driven
- fixture-driven
- intentionally-heavy
BudgetDecisionStatus:
type: string
enum:
- pending
- recovered
- recalibrated
HotspotStatus:
type: string
enum:
- seeded
- decomposed
- slimmed
- retained
- follow-up
PriorityTier:
type: string
enum:
- primary
- secondary
- residual
ResidualCostSource:
type: string
enum:
- family-breadth
- helper-driven
- fixture-driven
- mixed
- intentional-depth
RecommendedAction:
type: string
enum:
- split-family
- centralize-work
- narrow-assertions
- retain-as-heavy
- route-follow-up
SlimmingDecisionType:
type: string
enum:
- split
- centralize
- trim-duplicate-work
- retain
- follow-up
BudgetStatus:
type: string
enum:
- within-budget
- warning
- over-budget
HeavyGovernanceBudgetContract:
type: object
additionalProperties: false
required:
- laneId
- summaryThresholdSeconds
- evaluationThresholdSeconds
- normalizedThresholdSeconds
- baselineSource
- enforcementLevel
- lifecycleState
- decisionStatus
properties:
laneId:
type: string
const: heavy-governance
summaryThresholdSeconds:
type: number
exclusiveMinimum: 0
evaluationThresholdSeconds:
type: number
exclusiveMinimum: 0
normalizedThresholdSeconds:
type: number
exclusiveMinimum: 0
baselineSource:
type: string
enforcementLevel:
type: string
enum:
- report-only
- warn
- hard-fail
lifecycleState:
type: string
enum:
- draft
- documented
- recalibrated
reconciliationRationale:
type: string
decisionStatus:
$ref: '#/components/schemas/BudgetDecisionStatus'
HeavyGovernanceHotspot:
type: object
additionalProperties: false
required:
- familyId
- classificationId
- purpose
- measuredSeconds
- hotspotFiles
- costDriverCategory
- priorityTier
- status
properties:
familyId:
type: string
classificationId:
$ref: '#/components/schemas/ClassificationId'
purpose:
type: string
measuredSeconds:
type: number
minimum: 0
hotspotFiles:
type: array
minItems: 1
items:
type: string
costDriverCategory:
$ref: '#/components/schemas/CostDriverCategory'
priorityTier:
$ref: '#/components/schemas/PriorityTier'
currentBudgetSeconds:
type: number
minimum: 0
status:
$ref: '#/components/schemas/HotspotStatus'
FamilyCostDecomposition:
type: object
additionalProperties: false
required:
- familyId
- trustType
- requiredBreadth
- duplicateWorkSources
- residualCostSource
- recommendedAction
- notes
properties:
familyId:
type: string
trustType:
type: string
enum:
- workflow-trust
- surface-trust
- guard-trust
- discovery-trust
requiredBreadth:
type: string
duplicateWorkSources:
type: array
items:
type: string
duplicateWorkEstimateSeconds:
type: number
minimum: 0
residualCostSource:
$ref: '#/components/schemas/ResidualCostSource'
recommendedAction:
$ref: '#/components/schemas/RecommendedAction'
notes:
type: string
HeavyFamilySlimmingDecision:
type: object
additionalProperties: false
required:
- familyId
- decisionType
- guardPreservationPlan
- owner
- validationPlan
properties:
familyId:
type: string
decisionType:
$ref: '#/components/schemas/SlimmingDecisionType'
scope:
type: array
items:
type: string
guardPreservationPlan:
type: string
expectedDeltaSeconds:
type: number
owner:
type: string
validationPlan:
type: array
minItems: 1
items:
type: string
BudgetRecoverySnapshot:
type: object
additionalProperties: false
required:
- snapshotId
- capturedAt
- wallClockSeconds
- classificationTotals
- familyTotals
- slowestEntries
- artifactPaths
- budgetStatus
properties:
snapshotId:
type: string
capturedAt:
type: string
format: date-time
wallClockSeconds:
type: number
minimum: 0
classificationTotals:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- classificationId
- totalWallClockSeconds
properties:
classificationId:
$ref: '#/components/schemas/ClassificationId'
totalWallClockSeconds:
type: number
minimum: 0
familyTotals:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- familyId
- totalWallClockSeconds
properties:
familyId:
type: string
totalWallClockSeconds:
type: number
minimum: 0
slowestEntries:
type: array
minItems: 1
items:
type: object
additionalProperties: false
required:
- label
- wallClockSeconds
properties:
label:
type: string
wallClockSeconds:
type: number
minimum: 0
artifactPaths:
type: object
additionalProperties: false
required:
- summary
- budget
- report
properties:
summary:
type: string
pattern: ^storage/logs/test-lanes/
budget:
type: string
pattern: ^storage/logs/test-lanes/
report:
type: string
pattern: ^storage/logs/test-lanes/
budgetStatus:
$ref: '#/components/schemas/BudgetStatus'
BudgetOutcomeRecord:
type: object
additionalProperties: false
required:
- outcomeId
- decisionStatus
- finalThresholdSeconds
- finalMeasuredSeconds
- deltaSeconds
- deltaPercent
- remainingOpenFamilies
- justification
properties:
outcomeId:
type: string
decisionStatus:
$ref: '#/components/schemas/BudgetDecisionStatus'
finalThresholdSeconds:
type: number
exclusiveMinimum: 0
finalMeasuredSeconds:
type: number
minimum: 0
deltaSeconds:
type: number
deltaPercent:
type: number
remainingOpenFamilies:
type: array
items:
type: string
justification:
type: string
followUpDebt:
type: array
items:
type: string
HeavyAuthorGuidanceRule:
type: object
additionalProperties: false
required:
- ruleId
- whenToUse
- requiredDecision
- antiPattern
- preferredOutcome
properties:
ruleId:
type: string
whenToUse:
type: string
requiredDecision:
type: string
antiPattern:
type: string
preferredOutcome:
type: string

View File

@ -0,0 +1,565 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://tenantatlas.local/schemas/heavy-governance-hotspot-inventory.schema.json",
"title": "HeavyGovernanceHotspotInventory",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"artifactDirectory",
"budgetContract",
"hotspotInventory",
"decompositionRecords",
"slimmingDecisions",
"budgetSnapshots",
"budgetOutcome",
"authorGuidance"
],
"properties": {
"version": {
"type": "integer",
"minimum": 1
},
"artifactDirectory": {
"type": "string",
"const": "storage/logs/test-lanes"
},
"budgetContract": {
"$ref": "#/$defs/budgetContract"
},
"hotspotInventory": {
"type": "array",
"minItems": 5,
"items": {
"$ref": "#/$defs/hotspotInventoryRecord"
},
"allOf": [
{
"contains": {
"type": "object",
"required": ["familyId"],
"properties": {
"familyId": {
"const": "baseline-profile-start-surfaces"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["familyId"],
"properties": {
"familyId": {
"const": "findings-workflow-surfaces"
}
}
}
},
{
"contains": {
"type": "object",
"required": ["familyId"],
"properties": {
"familyId": {
"const": "finding-bulk-actions-workflow"
}
}
}
}
]
},
"decompositionRecords": {
"type": "array",
"minItems": 3,
"items": {
"$ref": "#/$defs/decompositionRecord"
}
},
"slimmingDecisions": {
"type": "array",
"minItems": 3,
"items": {
"$ref": "#/$defs/slimmingDecision"
}
},
"budgetSnapshots": {
"type": "array",
"minItems": 2,
"items": {
"$ref": "#/$defs/budgetSnapshot"
}
},
"budgetOutcome": {
"$ref": "#/$defs/budgetOutcome"
},
"authorGuidance": {
"type": "array",
"minItems": 4,
"items": {
"$ref": "#/$defs/authorGuidanceRule"
}
}
},
"$defs": {
"classificationId": {
"type": "string",
"enum": [
"ui-workflow",
"surface-guard",
"discovery-heavy"
]
},
"costDriverCategory": {
"type": "string",
"enum": [
"overbroad",
"redundant",
"discovery-heavy",
"workflow-heavy",
"surface-heavy",
"helper-driven",
"fixture-driven",
"intentionally-heavy"
]
},
"budgetDecisionStatus": {
"type": "string",
"enum": [
"pending",
"recovered",
"recalibrated"
]
},
"hotspotStatus": {
"type": "string",
"enum": [
"seeded",
"decomposed",
"slimmed",
"retained",
"follow-up"
]
},
"priorityTier": {
"type": "string",
"enum": [
"primary",
"secondary",
"residual"
]
},
"residualCostSource": {
"type": "string",
"enum": [
"family-breadth",
"helper-driven",
"fixture-driven",
"mixed",
"intentional-depth"
]
},
"recommendedAction": {
"type": "string",
"enum": [
"split-family",
"centralize-work",
"narrow-assertions",
"retain-as-heavy",
"route-follow-up"
]
},
"slimmingDecisionType": {
"type": "string",
"enum": [
"split",
"centralize",
"trim-duplicate-work",
"retain",
"follow-up"
]
},
"budgetStatus": {
"type": "string",
"enum": [
"within-budget",
"warning",
"over-budget"
]
},
"budgetContract": {
"type": "object",
"additionalProperties": false,
"required": [
"laneId",
"summaryThresholdSeconds",
"evaluationThresholdSeconds",
"normalizedThresholdSeconds",
"baselineSource",
"enforcementLevel",
"lifecycleState",
"decisionStatus"
],
"properties": {
"laneId": {
"type": "string",
"const": "heavy-governance"
},
"summaryThresholdSeconds": {
"type": "number",
"exclusiveMinimum": 0
},
"evaluationThresholdSeconds": {
"type": "number",
"exclusiveMinimum": 0
},
"normalizedThresholdSeconds": {
"type": "number",
"exclusiveMinimum": 0
},
"baselineSource": {
"type": "string"
},
"enforcementLevel": {
"type": "string",
"enum": ["report-only", "warn", "hard-fail"]
},
"lifecycleState": {
"type": "string",
"enum": ["draft", "documented", "recalibrated"]
},
"reconciliationRationale": {
"type": "string"
},
"decisionStatus": {
"$ref": "#/$defs/budgetDecisionStatus"
}
}
},
"hotspotInventoryRecord": {
"type": "object",
"additionalProperties": false,
"required": [
"familyId",
"classificationId",
"purpose",
"measuredSeconds",
"hotspotFiles",
"costDriverCategory",
"priorityTier",
"status"
],
"properties": {
"familyId": {
"type": "string",
"minLength": 1
},
"classificationId": {
"$ref": "#/$defs/classificationId"
},
"purpose": {
"type": "string",
"minLength": 1
},
"measuredSeconds": {
"type": "number",
"minimum": 0
},
"hotspotFiles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"costDriverCategory": {
"$ref": "#/$defs/costDriverCategory"
},
"priorityTier": {
"$ref": "#/$defs/priorityTier"
},
"currentBudgetSeconds": {
"type": "number",
"minimum": 0
},
"status": {
"$ref": "#/$defs/hotspotStatus"
}
}
},
"decompositionRecord": {
"type": "object",
"additionalProperties": false,
"required": [
"familyId",
"trustType",
"requiredBreadth",
"duplicateWorkSources",
"residualCostSource",
"recommendedAction",
"notes"
],
"properties": {
"familyId": {
"type": "string"
},
"trustType": {
"type": "string",
"enum": ["workflow-trust", "surface-trust", "guard-trust", "discovery-trust"]
},
"requiredBreadth": {
"type": "string",
"minLength": 1
},
"duplicateWorkSources": {
"type": "array",
"items": {
"type": "string"
}
},
"duplicateWorkEstimateSeconds": {
"type": "number",
"minimum": 0
},
"residualCostSource": {
"$ref": "#/$defs/residualCostSource"
},
"recommendedAction": {
"$ref": "#/$defs/recommendedAction"
},
"notes": {
"type": "string",
"minLength": 1
}
}
},
"slimmingDecision": {
"type": "object",
"additionalProperties": false,
"required": [
"familyId",
"decisionType",
"guardPreservationPlan",
"owner",
"validationPlan"
],
"properties": {
"familyId": {
"type": "string"
},
"decisionType": {
"$ref": "#/$defs/slimmingDecisionType"
},
"scope": {
"type": "array",
"items": {
"type": "string"
}
},
"guardPreservationPlan": {
"type": "string",
"minLength": 1
},
"expectedDeltaSeconds": {
"type": "number"
},
"owner": {
"type": "string",
"minLength": 1
},
"validationPlan": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
},
"budgetSnapshot": {
"type": "object",
"additionalProperties": false,
"required": [
"snapshotId",
"capturedAt",
"wallClockSeconds",
"classificationTotals",
"familyTotals",
"slowestEntries",
"artifactPaths",
"budgetStatus"
],
"properties": {
"snapshotId": {
"type": "string",
"minLength": 1
},
"capturedAt": {
"type": "string",
"format": "date-time"
},
"wallClockSeconds": {
"type": "number",
"minimum": 0
},
"classificationTotals": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["classificationId", "totalWallClockSeconds"],
"properties": {
"classificationId": {
"$ref": "#/$defs/classificationId"
},
"totalWallClockSeconds": {
"type": "number",
"minimum": 0
}
}
}
},
"familyTotals": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["familyId", "totalWallClockSeconds"],
"properties": {
"familyId": {
"type": "string"
},
"totalWallClockSeconds": {
"type": "number",
"minimum": 0
}
}
}
},
"slowestEntries": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["label", "wallClockSeconds"],
"properties": {
"label": {
"type": "string"
},
"wallClockSeconds": {
"type": "number",
"minimum": 0
}
}
}
},
"artifactPaths": {
"type": "object",
"additionalProperties": false,
"required": ["summary", "budget", "report"],
"properties": {
"summary": {
"type": "string",
"pattern": "^storage/logs/test-lanes/"
},
"budget": {
"type": "string",
"pattern": "^storage/logs/test-lanes/"
},
"report": {
"type": "string",
"pattern": "^storage/logs/test-lanes/"
}
}
},
"budgetStatus": {
"$ref": "#/$defs/budgetStatus"
}
}
},
"budgetOutcome": {
"type": "object",
"additionalProperties": false,
"required": [
"outcomeId",
"decisionStatus",
"finalThresholdSeconds",
"finalMeasuredSeconds",
"deltaSeconds",
"deltaPercent",
"remainingOpenFamilies",
"justification"
],
"properties": {
"outcomeId": {
"type": "string",
"minLength": 1
},
"decisionStatus": {
"$ref": "#/$defs/budgetDecisionStatus"
},
"finalThresholdSeconds": {
"type": "number",
"exclusiveMinimum": 0
},
"finalMeasuredSeconds": {
"type": "number",
"minimum": 0
},
"deltaSeconds": {
"type": "number"
},
"deltaPercent": {
"type": "number"
},
"remainingOpenFamilies": {
"type": "array",
"items": {
"type": "string"
}
},
"justification": {
"type": "string",
"minLength": 1
},
"followUpDebt": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"authorGuidanceRule": {
"type": "object",
"additionalProperties": false,
"required": ["ruleId", "whenToUse", "requiredDecision", "antiPattern", "preferredOutcome"],
"properties": {
"ruleId": {
"type": "string",
"minLength": 1
},
"whenToUse": {
"type": "string",
"minLength": 1
},
"requiredDecision": {
"type": "string",
"minLength": 1
},
"antiPattern": {
"type": "string",
"minLength": 1
},
"preferredOutcome": {
"type": "string",
"minLength": 1
}
}
}
}
}

View File

@ -0,0 +1,193 @@
# Data Model: Heavy Governance Lane Cost Reduction
This feature does not introduce new runtime database tables. The data-model work formalizes repository-level governance objects that describe how the heavy-governance lane is inventoried, decomposed, slimmed, and evaluated against a single explicit budget contract. It builds directly on the existing Spec 206 to 208 lane manifest and report artifacts.
## 1. Heavy Governance Budget Contract
### Purpose
Represents the deliberate budget rule for the heavy-governance lane, including the authoritative pre-normalization `300s` summary threshold, the legacy `200s` detailed budget-target evaluation, and the final reconciled threshold published by Spec 209.
### Fields
- `laneId`: expected to be `heavy-governance`
- `summaryThresholdSeconds`: current authoritative pre-normalization lane-level threshold used by the summary artifact
- `evaluationThresholdSeconds`: legacy threshold currently used in detailed budget evaluations until normalization is complete
- `normalizedThresholdSeconds`: the single threshold that will be treated as authoritative after Spec 209
- `baselineSource`: where the budget came from, such as `measured-lane`
- `enforcementLevel`: `report-only`, `warn`, or `hard-fail`
- `lifecycleState`: `draft`, `documented`, or `recalibrated`
- `reconciliationRationale`: explanation of why the single threshold is correct for the post-Spec-209 lane
- `decisionStatus`: `pending`, `recovered`, or `recalibrated`
### Validation rules
- `laneId` must be `heavy-governance`.
- `summaryThresholdSeconds` and `evaluationThresholdSeconds` must both be captured when they differ, and the authoritative pre-normalization contract must remain identifiable until normalization is complete.
- `normalizedThresholdSeconds` must be present before the rollout is considered complete.
- `decisionStatus = recovered` requires measured runtime less than or equal to `normalizedThresholdSeconds`.
- `decisionStatus = recalibrated` requires a non-empty `reconciliationRationale`.
## 2. Heavy Governance Hotspot Inventory Record
### Purpose
Represents one named heavy-governance family that materially contributes to the lane's runtime and needs explicit review.
### Fields
- `familyId`: stable family identifier
- `classificationId`: current owning class such as `ui-workflow`, `surface-guard`, or `discovery-heavy`
- `purpose`: why the family exists and what trust it provides
- `measuredSeconds`: current measured contribution from the latest heavy-governance report
- `hotspotFiles`: dominant files tied to the family
- `costDriverCategory`: primary cause such as `overbroad`, `redundant`, `discovery-heavy`, `workflow-heavy`, `surface-heavy`, `helper-driven`, `fixture-driven`, or `intentionally-heavy`
- `priorityTier`: `primary`, `secondary`, or `residual`
- `currentBudgetSeconds`: current family-level budget if one exists
- `status`: `seeded`, `decomposed`, `slimmed`, `retained`, or `follow-up`
### Validation rules
- `hotspotFiles` must contain at least one file.
- `priorityTier = primary` is required for the families that explain most of the lane runtime.
- The full hotspot inventory must cover the current top 5 families by runtime, or enough families to explain at least 80% of lane runtime, whichever set is larger.
- `status = slimmed` or `status = retained` requires a corresponding decomposition record.
- If `currentBudgetSeconds` exists, it must match the checked-in family budget contract.
## 3. Family Cost Decomposition Record
### Purpose
Represents the internal analysis for a targeted hotspot family so reviewers can see what part of the family is necessary and what part is duplicated or accidental.
### Fields
- `familyId`: referenced hotspot family
- `trustType`: primary trust delivered, such as `workflow-trust`, `surface-trust`, `guard-trust`, or `discovery-trust`
- `requiredBreadth`: what breadth is genuinely needed for product trust
- `duplicateWorkSources`: repeated work sources such as `repeated-livewire-mounts`, `header-action-gating-matrix`, `filter-state-persistence`, `audit-fan-out`, `resource-discovery-pass`, or `helper-graph-build`
- `duplicateWorkEstimateSeconds`: optional estimate of removable cost
- `residualCostSource`: `family-breadth`, `helper-driven`, `fixture-driven`, `mixed`, or `intentional-depth`
- `recommendedAction`: `split-family`, `centralize-work`, `narrow-assertions`, `retain-as-heavy`, or `route-follow-up`
- `notes`: reviewer-readable explanation
### Validation rules
- Every `primary` hotspot family must have one decomposition record.
- `recommendedAction = route-follow-up` requires `residualCostSource` to be `helper-driven`, `fixture-driven`, or `mixed`.
- `recommendedAction = retain-as-heavy` requires `residualCostSource = intentional-depth`.
- `duplicateWorkEstimateSeconds` may be omitted when cost is truly intentional, but the reason must be explicit.
## 4. Heavy Family Slimming Decision
### Purpose
Represents the implementation-facing decision taken for a hotspot family after decomposition.
### Fields
- `familyId`: referenced hotspot family
- `decisionType`: `split`, `centralize`, `trim-duplicate-work`, `retain`, or `follow-up`
- `scope`: list of files, helpers, or manifest entries touched by the decision
- `guardPreservationPlan`: how the original governance trust remains protected
- `expectedDeltaSeconds`: estimated improvement if known
- `owner`: responsible maintainer or team role
- `validationPlan`: focused tests or lane reruns needed to validate the decision
### Validation rules
- Every `decisionType` other than `retain` must include at least one item in `scope`.
- `guardPreservationPlan` is mandatory for all decisions.
- `follow-up` decisions must name the residual cause and target seam.
- `retain` decisions must still reference a validation plan showing why the retained heaviness is acceptable.
## 5. Budget Recovery Snapshot
### Purpose
Represents one snapshot in the before-and-after lane measurement pair used to prove recovery or justify recalibration.
### Fields
- `snapshotId`: stable identifier such as `pre-slimming` or `post-slimming`
- `capturedAt`: ISO timestamp
- `wallClockSeconds`: measured heavy-governance wall-clock time
- `classificationTotals`: totals by classification
- `familyTotals`: totals by family
- `slowestEntries`: top slowest test entries
- `artifactPaths`: references to summary, report, and budget artifacts
- `budgetStatus`: `within-budget`, `warning`, or `over-budget`
### Validation rules
- At least two snapshots are expected for a complete rollout: baseline and post-change.
- `artifactPaths` must stay under `storage/logs/test-lanes`.
- `familyTotals` must include the targeted hotspot families.
- Summary, budget, and report artifacts captured for the same snapshot must not disagree on the authoritative threshold or budget outcome classification.
## 6. Budget Outcome Record
### Purpose
Represents the final explicit outcome required by Spec 209.
### Fields
- `outcomeId`: stable identifier
- `decisionStatus`: `recovered` or `recalibrated`
- `finalThresholdSeconds`: authoritative heavy-governance threshold after the rollout
- `finalMeasuredSeconds`: measured post-change runtime
- `deltaSeconds`: change from the baseline snapshot
- `deltaPercent`: percentage change from the baseline snapshot
- `remainingOpenFamilies`: families still above expected cost or still awaiting follow-up
- `justification`: human-readable explanation of the decision
- `followUpDebt`: optional residual items that remain outside the current scope
### Validation rules
- `decisionStatus = recovered` requires `finalMeasuredSeconds <= finalThresholdSeconds`.
- `decisionStatus = recalibrated` requires a non-empty `justification` explaining why the new threshold is honest.
- `remainingOpenFamilies` may be non-empty only when their residual status is explicit.
## 7. Heavy Author Guidance Rule
### Purpose
Represents a short reviewer or author rule for future heavy-governance tests.
### Fields
- `ruleId`: stable identifier
- `whenToUse`: the situation the rule applies to
- `requiredDecision`: what the author or reviewer must decide
- `antiPattern`: what overbroad behavior the rule prevents
- `preferredOutcome`: the intended family or separation behavior
### Validation rules
- Guidance must cover at least: when to create a new heavy family, when to reuse an existing family, when a test is too broad, and when discovery, workflow, and surface trust must be separated.
## 8. Current Measured Inventory Snapshot
### Current dominant families
- `baseline-profile-start-surfaces``98.112193s``ui-workflow` — currently the largest heavy-governance family
- `action-surface-contract``40.841552s``surface-guard`
- `ops-ux-governance``38.794861s``surface-guard`
- `findings-workflow-surfaces``36.459493s``ui-workflow`
- `finding-bulk-actions-workflow``26.491446s``ui-workflow`
- `workspace-settings-slice-management``21.740839s``ui-workflow`
### Current classification totals
- `ui-workflow``190.606431s`
- `surface-guard``106.845887s`
- `discovery-heavy``0.863003s`
### Current budget signals
- Lane summary threshold: `300s` and currently the authoritative pre-normalization contract
- Budget target evaluation threshold: `200s` and currently legacy drift evidence, not a second passing threshold
- Current measured lane wall clock: `318.296962s`
This dual-signal state is intentional input to Spec 209 and must be resolved by the final budget outcome.

View File

@ -0,0 +1,168 @@
# Implementation Plan: Heavy Governance Lane Cost Reduction
**Branch**: `209-heavy-governance-cost` | **Date**: 2026-04-17 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/209-heavy-governance-cost/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/209-heavy-governance-cost/spec.md`
## Summary
Build Spec 209 on top of the existing Spec 206, 207, and 208 lane infrastructure by using the current heavy-governance artifact set as the baseline, treating the current `300s` lane summary threshold as the authoritative pre-normalization contract while the `200s` `budgetTargets()` signal remains legacy drift evidence to be reconciled, decomposing the dominant heavy families by trust type and duplicated work, targeting the ui-workflow hotspots first, treating surface-guard families as intentional heavy checks unless repeatable redundancy is proven, and ending with explicit budget recovery or explicit recalibration evidence without moving heavy cost back into lighter lanes.
## Technical Context
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
**Storage**: SQLite `:memory:` for the default test environment, mixed database strategy for some heavy-governance families as declared in `TestLaneManifest`, and existing lane artifacts under the app-root contract path `storage/logs/test-lanes`
**Testing**: Pest unit, feature, browser, architecture, and guard suites run through Sail-wrapped `artisan test`; heavy-lane selection and reporting already flow through `Tests\Support\TestLaneManifest`, `Tests\Support\TestLaneBudget`, `Tests\Support\TestLaneReport`, `tests/Pest.php`, and the repo-root wrappers `scripts/platform-test-lane` and `scripts/platform-test-report`
**Target Platform**: Laravel monorepo application in `apps/platform`, executed locally through Sail and later enforced in shared CI
**Project Type**: Monorepo with a Laravel platform app and separate Astro website; this feature is scoped to platform test-governance infrastructure
**Performance Goals**: Recover the heavy-governance lane from the current `318.296962s` run to the authoritative pre-normalization heavy-lane threshold of `300s`, or explicitly recalibrate that threshold after evidence is gathered; explain at least 80% of heavy-lane runtime through named families; reduce duplicate work or accidental breadth in the top hotspot families without reducing governance trust
**Constraints**: Sail-first commands only; no new product routes, assets, runtime services, or dependencies; no browser-lane redesign; no CI-matrix rollout; no lane-hiding by moving heavy families into Confidence or Fast Feedback; preserve Heavy Governance lane membership for touched families unless a non-budget, spec-backed rationale is recorded; treat the current `300s` lane summary threshold as the authoritative pre-normalization contract while the `200s` lane budget-target evaluation remains legacy drift evidence to be normalized
**Scale/Scope**: Current heavy-governance reporting attributes 14 named families; `ui-workflow` accounts for `190.606431s`, `surface-guard` for `106.845887s`, and `discovery-heavy` for `0.863003s`; the dominant family hotspots are `baseline-profile-start-surfaces` (`98.112193s`), `action-surface-contract` (`40.841552s`), `ops-ux-governance` (`38.794861s`), `findings-workflow-surfaces` (`36.459493s`), `finding-bulk-actions-workflow` (`26.491446s`), and `workspace-settings-slice-management` (`21.740839s`)
### Filament v5 Implementation Notes
- **Livewire v4.0+ compliance**: Preserved. This feature changes only repository test-governance around Filament and Livewire-heavy tests, not runtime Filament or Livewire behavior.
- **Provider registration location**: Unchanged. Existing panel providers remain registered in `bootstrap/providers.php`.
- **Global search rule**: No globally searchable resources are added or modified. Discovery-heavy parity tests may be reclassified or slimmed, but runtime global-search behavior is unchanged.
- **Destructive actions**: No runtime destructive actions are introduced. Any tests touched by this feature continue to validate existing confirmation and authorization behavior only.
- **Asset strategy**: No panel-only or shared assets are added. Existing `filament:assets` deployment behavior remains unchanged.
- **Testing plan**: Add or update Pest guard coverage for heavy-hotspot inventory integrity, budget-signal consistency, family decomposition records, heavy-lane budget outcome reporting, and targeted hotspot family regression checks. Focused validation should cover the targeted families plus the heavy-governance lane wrapper.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: PASS. No Inventory, snapshots, or backup truth is changed.
- Read/write separation: PASS. The feature only changes repository test-governance behavior and introduces no end-user mutation path.
- Graph contract path: PASS. No Graph calls, contract-registry changes, or provider runtime integrations are added.
- Deterministic capabilities: PASS. No capability resolver or authorization registry changes.
- RBAC-UX, workspace isolation, tenant isolation: PASS. No runtime routes, policies, global search availability, or tenant/workspace enforcement semantics are changed.
- Run observability and Ops-UX: PASS. Reporting remains filesystem-based through the existing lane tooling and does not introduce `OperationRun` behavior.
- Data minimization: PASS. Heavy-lane inventories and reports remain repo-local and contain no secrets or customer payloads.
- Proportionality and bloat control: PASS WITH LIMITS. The only new semantic layer is a narrow repo-local hotspot inventory and decomposition model. The plan explicitly avoids a broader framework and keeps family changes tied to measured cost and guard preservation.
- TEST-TRUTH-001: PASS WITH WORK. The plan must prove that runtime gains come from removing duplicated work or accidental breadth rather than from quietly deleting governance trust.
- Filament/UI constitutions: PASS / NOT APPLICABLE. No operator-facing UI, action-surface runtime contract, badge semantics, or panel IA is changed.
**Phase 0 Gate Result**: PASS
- The feature remains bounded to repository test governance, hotspot evidence, family decomposition, and budget normalization.
- No new runtime persistence, product routes, panels, assets, or Graph seams are introduced.
- The chosen approach extends the existing Spec 206 to 208 tooling instead of creating a second heavy-lane governance system.
## Project Structure
### Documentation (this feature)
```text
specs/209-heavy-governance-cost/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ ├── heavy-governance-hotspot-inventory.schema.json
│ └── heavy-governance-cost-recovery.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
apps/
├── platform/
│ ├── composer.json
│ ├── tests/
│ │ ├── Pest.php
│ │ ├── Support/
│ │ │ ├── TestLaneBudget.php
│ │ │ ├── TestLaneManifest.php
│ │ │ └── TestLaneReport.php
│ │ ├── Feature/
│ │ │ ├── Baselines/
│ │ │ ├── Drift/
│ │ │ ├── Filament/
│ │ │ ├── Findings/
│ │ │ ├── Guards/
│ │ │ ├── OpsUx/
│ │ │ ├── Rbac/
│ │ │ └── SettingsFoundation/
│ └── storage/logs/test-lanes/
├── website/
└── ...
scripts/
├── platform-test-lane
└── platform-test-report
```
**Structure Decision**: Keep implementation concentrated in the existing platform test-governance seams: `apps/platform/tests/Support/TestLaneManifest.php` for hotspot inventory, family budgets, and budget-signal normalization; `apps/platform/tests/Support/TestLaneReport.php` for before-and-after attribution and explicit budget outcomes; focused heavy families under `apps/platform/tests/Feature/Baselines`, `apps/platform/tests/Feature/Filament`, `apps/platform/tests/Feature/Findings`, `apps/platform/tests/Feature/Guards`, `apps/platform/tests/Feature/OpsUx`, and `apps/platform/tests/Feature/SettingsFoundation`; and the existing repo-root wrappers for measurement. Planning artifacts stay inside `specs/209-heavy-governance-cost`.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: Maintainers and reviewers cannot tell which heavy-governance families are legitimately expensive, which are redundant, and which are simply overbroad, so the lane remains over budget without a stable correction path.
- **Existing structure is insufficient because**: Spec 208 established heavy-family ownership and attribution, but it did not yet decompose the internal cost of the dominant families or reconcile the current heavy-budget mismatch.
- **Narrowest correct implementation**: Extend the existing lane manifest, report artifacts, and heavy-family catalog with hotspot decomposition, explicit residual-cause records, a budget-outcome record, and targeted guidance for future heavy tests.
- **Ownership cost created**: The repo must maintain hotspot decomposition notes, the reconciled heavy-budget contract, and author or reviewer guidance as heavy families evolve.
- **Alternative intentionally rejected**: Local one-off runtime trims or moving families back into lighter lanes, because those approaches hide cost instead of making it governable.
- **Release truth**: Current-release repository truth and the necessary stabilization step before CI budget enforcement.
## Phase 0 — Research (complete)
- Output: [research.md](./research.md)
- Resolved key decisions:
- Reuse the existing heavy-governance manifest, budget, report, and wrapper seams rather than creating a second heavy-lane system.
- Treat the current heavy-governance artifact set as the baseline, specifically `318.296962s` wall-clock against the authoritative pre-normalization `300s` lane summary threshold.
- Make the dual heavy-budget signal explicit: the lane summary uses `300s` as the authoritative pre-normalization contract, while `budgetTargets()` still evaluates the lane against `200s` as legacy drift evidence that must be normalized.
- Prioritize `baseline-profile-start-surfaces`, `findings-workflow-surfaces`, and `finding-bulk-actions-workflow` as the first slimming targets because the `ui-workflow` classification currently dominates the lane.
- Decompose targeted families by repeated work before splitting files mechanically.
- Record helper-driven or fixture-driven cost as explicit residual debt when that is the real cause.
- Treat `action-surface-contract` and `ops-ux-governance` as intentional heavy second-wave candidates unless repeated duplication is proven.
- Keep before-and-after evidence inside the existing heavy-governance artifact set under `storage/logs/test-lanes`.
- End the feature with explicit recovery or explicit recalibration, not an implicit “still heavy” state.
## Phase 1 — Design & Contracts (complete)
- Output: [data-model.md](./data-model.md) formalizes the hotspot inventory, per-family cost decomposition, slimming decisions, dual-budget contract, explicit budget outcome, and author-guidance rule set.
- Output: [contracts/heavy-governance-hotspot-inventory.schema.json](./contracts/heavy-governance-hotspot-inventory.schema.json) defines the checked-in schema for the heavy-governance hotspot inventory, decomposition records, budget signals, and final outcome contract.
- Output: [contracts/heavy-governance-cost-recovery.logical.openapi.yaml](./contracts/heavy-governance-cost-recovery.logical.openapi.yaml) captures the logical contract for reading hotspots, recording family decomposition, evaluating budget outcomes, and publishing reviewer guidance.
- Output: [quickstart.md](./quickstart.md) provides the rollout order, validation commands, and review checkpoints for the cost-recovery work.
### Post-design Constitution Re-check
- PASS: No runtime routes, panels, authorization planes, or Graph seams are introduced.
- PASS: The new hotspot inventory and budget-outcome records are repo-local, directly justified by current lane drift, and bounded to existing test-governance seams.
- PASS: The design prefers extending existing manifest, guard, and reporting seams over adding a second governance framework.
- PASS WITH WORK: The final implementation must normalize the conflicting heavy-lane budget signals so reviewers see one intentional contract instead of two competing thresholds.
- PASS WITH WORK: The final implementation must show that heavy runtime improvements came from duplicate-work removal or narrower family scope, not from hidden trust reduction.
## Phase 2 — Implementation Planning
`tasks.md` should cover:
- Refreshing the current heavy-governance baseline artifact set through the standard lane wrappers before any family edits.
- Building a checked-in hotspot inventory that covers the current top 5 families by runtime, or enough families to explain at least 80% of lane runtime, whichever set is larger.
- Auditing `baseline-profile-start-surfaces`, `findings-workflow-surfaces`, and `finding-bulk-actions-workflow` for repeated Livewire mounts, gating matrices, filter persistence, audit fan-out, and helper-driven cost.
- Deciding for each targeted family whether the right fix is splitting, centralizing repeated work, or recording explicit intentional heaviness.
- Auditing second-wave surface-guard families such as `action-surface-contract` and `ops-ux-governance` for internal redundancy only after the workflow-heavy hotspots are addressed.
- Extending `TestLaneManifest` and `TestLaneReport` so hotspot inventory, residual causes, and budget outcomes stay reviewable and attributable.
- Adding or updating guard tests that protect budget-signal consistency, hotspot-inventory integrity, the top-5-or-80%-coverage rule, lane-membership invariants for touched heavy families, and future heavy-family authoring discipline.
- Normalizing the heavy-governance budget contract from the authoritative pre-normalization `300s` summary threshold and the legacy `200s` `budgetTargets()` signal to one deliberate rule after the hotspot inventory and slimming pass have established the honest lane shape.
- Rerunning the heavy-governance lane and its focused hotspot packs to produce post-change summary, report, and budget artifacts.
- Recording the final budget decision as explicit recovery within the authoritative threshold or explicit recalibration with evidence.
### Contract Implementation Note
- The JSON schema is schema-first and repository-tooling-oriented. It defines what the checked-in hotspot inventory, decomposition records, budget contract, and final budget outcome must express even if the first implementation remains PHP arrays in `TestLaneManifest` and JSON output from `TestLaneReport`.
- The OpenAPI file is logical rather than transport-prescriptive. It documents the expected semantics of hotspot inspection, decomposition review, budget-outcome evaluation, and author guidance for in-process repository tooling.
- The design intentionally avoids introducing a new runtime service, new database table, or new artifact root outside the existing `storage/logs/test-lanes` contract.
### Deployment Sequencing Note
- No database migration is planned.
- No asset publish step changes.
- The rollout should start with a fresh heavy-governance baseline capture through the standard wrappers, then hotspot decomposition, then family-level slimming or centralization, then budget-signal normalization, and finally a rerun whose summary, budget, and report artifacts agree on one authoritative threshold and one explicit outcome.

View File

@ -0,0 +1,151 @@
# Quickstart: Heavy Governance Lane Cost Reduction
## Goal
Stabilize the heavy-governance lane so its dominant costs are visible, intentionally sliced, and either brought back within the authoritative heavy-lane budget, which starts at `300s` before normalization, or consciously recalibrated with evidence.
## Current Outcome
The latest honest rerun ends in explicit recalibration rather than recovery.
| Signal | Current value | Meaning |
|-------|---------------|---------|
| Final wall clock | `329.305382s` | Current heavy-governance lane runtime after the slimming pass |
| Final authoritative threshold | `330s` | Normalized threshold used consistently by summary, budget, and report artifacts |
| Outcome | `recalibrated` | The lane no longer has dual active thresholds, but it still needs a slightly higher honest contract |
| Baseline delta | `+11.008420s` (`+3.458905%`) | Current rerun versus the preserved pre-slimming baseline |
| Legacy drift signal | `200s` | Preserved as historical detailed-budget evidence only |
| Pre-normalization summary threshold | `300s` | Preserved as the rollout acceptance contract before normalization |
The final reconciled rationale is: workflow-heavy duplication was reduced, but the settled lane still retains intentional surface-guard depth plus the workspace settings residual helper cost, so the contract is now `330s`.
## Implementation Order
1. Capture a fresh heavy-governance baseline through the existing lane wrappers and preserve the current summary, report, and budget artifacts. The checked-in wrappers now support `--capture-baseline` for heavy-governance baseline copies.
2. Build or refresh the hotspot inventory for the current top 5 families by runtime, or enough families to explain at least 80% of lane runtime, whichever set is larger.
3. Decompose the primary ui-workflow hotspots first: `baseline-profile-start-surfaces`, `findings-workflow-surfaces`, and `finding-bulk-actions-workflow`.
4. Decide per family whether the right move is split, centralize repeated work, trim duplicate assertions, or retain as intentionally heavy.
5. Audit second-wave surface-guard families such as `action-surface-contract` and `ops-ux-governance` only after the workflow-heavy hotspots are understood.
6. Extend or adjust manifest and report seams so decomposition, residual causes, and the final budget outcome remain visible.
7. Normalize the heavy-governance budget contract so the authoritative pre-normalization `300s` summary threshold and the legacy `200s` budget-target evaluation describe one intentional rule after the honest lane shape is established.
8. Rerun the focused hotspot packs and the full heavy-governance lane.
9. Record the final outcome as budget recovery or explicit recalibration and add short reviewer guidance for future heavy tests.
## Suggested Code Touches
```text
apps/platform/tests/Support/TestLaneBudget.php
apps/platform/tests/Support/TestLaneManifest.php
apps/platform/tests/Support/TestLaneReport.php
apps/platform/tests/Feature/Baselines/*
apps/platform/tests/Feature/Filament/BaselineActionAuthorizationTest.php
apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php
apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php
apps/platform/tests/Feature/Findings/*
apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php
apps/platform/tests/Feature/OpsUx/*
apps/platform/tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php
scripts/platform-test-lane
scripts/platform-test-report
```
## Validation Flow
Use the existing checked-in lane wrappers first:
```bash
./scripts/platform-test-report heavy-governance --capture-baseline
./scripts/platform-test-lane heavy-governance --capture-baseline
./scripts/platform-test-report heavy-governance
./scripts/platform-test-lane heavy-governance
./scripts/platform-test-report heavy-governance
cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
```
Keep the implementation loop tight with the most relevant focused suites before rerunning the whole lane:
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament --filter=BaselineProfileCaptureStartSurfaceTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament --filter=BaselineProfileCompareStartSurfaceTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament --filter=BaselineActionAuthorizationTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings --filter=FindingBulkActionsTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Findings --filter=FindingWorkflow
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards --filter=ActionSurfaceContractTest
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/OpsUx
```
## Current Baseline
Use the checked-in heavy-governance artifacts under `apps/platform/storage/logs/test-lanes` as the starting point.
| Signal | Current value | Planning note |
|-------|---------------|---------------|
| Lane wall clock | `318.296962s` | Current measured overrun |
| Lane summary threshold | `300s` | Authoritative pre-normalization contract for Spec 209 acceptance |
| Budget target evaluation threshold | `200s` | Legacy drift evidence that must remain visible until the contract is normalized |
| `ui-workflow` total | `190.606431s` | Dominant class; first slimming target |
| `surface-guard` total | `106.845887s` | Second-wave analysis target |
| `discovery-heavy` total | `0.863003s` | Already bounded; not the main cost problem |
## Current Canonical Inventory
The canonical inventory now covers six families because the top five alone do not clear the required `80%` runtime threshold.
| Family | Baseline measured time | Current status | Driver |
|-------|-------------------------|----------------|--------|
| `baseline-profile-start-surfaces` | `98.112193s` | `slimmed` | workflow-heavy |
| `action-surface-contract` | `40.841552s` | `retained` | intentionally-heavy |
| `ops-ux-governance` | `38.794861s` | `retained` | intentionally-heavy |
| `findings-workflow-surfaces` | `36.459493s` | `slimmed` | workflow-heavy |
| `finding-bulk-actions-workflow` | `26.491446s` | `slimmed` | redundant |
| `workspace-settings-slice-management` | `21.740839s` | `follow-up` | helper-driven |
Together these six families explain `263.617244s`, or `80.052516%`, of the latest heavy-governance runtime.
## Latest Rerun Hotspots
| Family | Latest measured time | Current intent |
|-------|------------------------|----------------|
| `baseline-profile-start-surfaces` | `101.895415s` | Still dominant after slimming; trust retained |
| `action-surface-contract` | `38.323501s` | Intentionally heavy and retained |
| `ops-ux-governance` | `36.497049s` | Intentionally heavy and retained |
| `findings-workflow-surfaces` | `35.990272s` | Slimmed, but still a meaningful workflow-heavy slice |
| `finding-bulk-actions-workflow` | `30.145259s` | Slimmed fixture fan-out, still a top single test family |
| `workspace-settings-slice-management` | `20.765748s` | Recorded as explicit follow-up debt |
## Decomposition Checklist
For each primary hotspot family, answer these questions before changing file structure:
1. What governance trust does this family deliver?
2. What breadth is genuinely required for that trust?
3. Which repeated work sources dominate runtime?
4. Is the main cost family-breadth, helper-driven setup, or fixture-driven setup?
5. Is the correct fix a split, a centralization, a duplicate-assertion trim, or intentional retention?
6. What focused tests and lane reruns prove the change did not hollow out governance trust?
## Reviewer Guidance Targets
The implementation should leave behind short rules that cover:
1. When a new heavy family is justified.
2. When a test should join an existing heavy family instead.
3. When discovery, workflow, and surface trust must be separated.
4. When a family should stay intentionally heavy.
5. When a helper or fixture cost must be recorded as residual debt instead of disguised as family improvement.
The canonical reviewer rules now live in `TestLaneManifest::heavyGovernanceAuthorGuidance()` and are:
1. `heavy-family-reuse-before-creation`
2. `heavy-family-create-only-for-new-trust`
3. `split-discovery-workflow-surface-concerns`
4. `retain-intentional-heavy-depth-explicitly`
5. `record-helper-or-fixture-residuals`
## Exit Criteria
1. The heavy-governance budget contract is normalized to one authoritative threshold, and the summary, budget, and report artifacts do not disagree about it.
2. The primary hotspot families have decomposition records and explicit slimming decisions.
3. The heavy-governance lane has fresh before and after evidence in the standard artifact paths, including inventory coverage for the top 5 families or at least 80% of runtime, whichever is larger.
4. The final outcome is explicit: recovered within the authoritative threshold for the rollout or consciously recalibrated.
5. Reviewer guidance exists for future heavy-family authoring.

View File

@ -0,0 +1,81 @@
# Research: Heavy Governance Lane Cost Reduction
## Decision 1: Reuse the existing heavy-lane governance seams instead of adding a new runner or metadata system
- Decision: Spec 209 should extend the existing `TestLaneManifest`, `TestLaneBudget`, `TestLaneReport`, `scripts/platform-test-lane`, and `scripts/platform-test-report` seams rather than introducing a second planning or reporting framework.
- Rationale: The repository already has checked-in heavy-family attribution, family budgets, lane budgets, and report artifacts under `apps/platform/storage/logs/test-lanes`. The missing work is family decomposition and budget recovery, not missing lane infrastructure.
- Alternatives considered:
- Add a separate heavy-cost analysis runner: rejected because it would duplicate the current test-governance contract.
- Use only ad-hoc profiling commands: rejected because Spec 209 requires repeatable before-and-after evidence and reviewer-visible outputs.
## Decision 2: Treat the feature as a budget-recovery exercise against the current measured heavy-governance overrun
- Decision: The planning baseline for Spec 209 is the current heavy-governance artifact set showing `318.296962` seconds wall-clock versus the authoritative pre-normalization lane summary budget of `300` seconds.
- Rationale: Spec 208 already moved the correct heavy families into the heavy-governance lane. The remaining issue is now an explicit cost-recovery problem, not a classification problem.
- Alternatives considered:
- Re-profile from scratch without using the current heavy artifact: rejected because the current artifact already captures the relevant runtime signal and hotspot attribution.
- Treat the lane as healthy because the overrun is relatively small: rejected because the spec requires an explicit budget outcome, not quiet acceptance of ongoing drift.
## Decision 3: Make the dual heavy-lane budget signal an explicit planning concern
- Decision: Spec 209 should explicitly reconcile the current heavy-governance budget mismatch by treating the lane summary threshold of `300` seconds as the authoritative pre-normalization contract and the separate `budgetTargets()` lane target of `200` seconds as legacy drift evidence until one normalized threshold is published.
- Rationale: The current report summary and the detailed budget evaluation do not describe the same target. A later CI budget-enforcement phase cannot be credible while that inconsistency exists.
- Alternatives considered:
- Ignore the mismatch and optimize only against the 300-second lane summary: rejected because the stricter 200-second target still appears in checked-in budget evaluations and will confuse reviewers.
- Force the lane to 200 seconds immediately: rejected because the spec first needs to determine whether the 200-second target is still realistic for the now-honest heavy lane.
## Decision 4: Prioritize the dominant ui-workflow families before second-wave surface guards
- Decision: The first slimming pass should prioritize `baseline-profile-start-surfaces`, `findings-workflow-surfaces`, and `finding-bulk-actions-workflow`, with `workspace-settings-slice-management` as the next workflow-heavy fallback if more recovery is required.
- Rationale: Current heavy-governance attribution shows `ui-workflow` at `190.606431` seconds, or roughly 60% of lane cost. The three named families together account for about `161.06` seconds and directly align with the spec's required hotspot set.
- Alternatives considered:
- Start with `action-surface-contract`: rejected as the first pass because it is clearly expensive but already documented as an intentional governance guard and may have less removable duplication than the workflow-heavy hotspots.
- Start with all surface-guard families equally: rejected because the current runtime evidence shows ui-workflow as the dominant cost bucket.
## Decision 5: Decompose targeted families by repeated work before splitting files mechanically
- Decision: Each targeted hotspot family should first be decomposed by repeated Livewire mounts, header-action gating matrices, filter-state persistence checks, bulk-action fan-out, evidence or audit verification, and any helper-driven fixture cost before deciding whether to split files.
- Rationale: Spec 209 is about real cost reduction, not cosmetic decomposition. A family can remain overbroad even after being split if the same expensive setup still runs in every resulting file.
- Alternatives considered:
- Split all top families immediately: rejected because that can produce cleaner file boundaries without removing the dominant repeated work.
- Only centralize helper setup without family-level analysis: rejected because some cost may be due to semantic breadth rather than helper shape.
## Decision 6: Record helper-driven or fixture-driven cost as residual debt instead of forcing a family explanation that is not true
- Decision: If a targeted hotspot is found to be dominated by helper, fixture, or support-path cost rather than family breadth, the resulting plan should record that as explicit residual debt and treat it as follow-up work instead of pretending the family itself was narrowed.
- Rationale: The spec requires honest attribution. Mislabeling helper or fixture cost as family-width improvement would create false confidence and make later budget work less reliable.
- Alternatives considered:
- Force all heavy cost into family-width categories: rejected because it would violate the spec's explicit residual-cause requirement.
- Reopen Spec 207 inside Spec 209: rejected because fixture slimming remains a separate concern even when its residual effects appear here.
## Decision 7: Treat `action-surface-contract` and `ops-ux-governance` as intentional heavy families unless decomposition exposes repeatable duplication
- Decision: `action-surface-contract` and `ops-ux-governance` should be treated as second-wave slimming candidates. They remain in heavy-governance by default and should only be narrowed where clear duplicate discovery or repeated governance passes can be shown.
- Rationale: Together they account for `79.636413` seconds and are meaningful heavy governance checks. They may still contain removable redundancy, but their default assumption should be “intentionally heavy until proven otherwise,” not “overbroad by default.”
- Alternatives considered:
- Treat all surface-guard cost as excessive: rejected because these families intentionally protect cross-resource governance contracts.
- Exclude them from the plan entirely: rejected because they are still major contributors to lane cost and may need second-pass analysis.
## Decision 8: Use the existing heavy-governance report artifacts as the before-and-after evidence contract
- Decision: Pre-change and post-change evidence should continue to flow through `heavy-governance-latest.summary.md`, `heavy-governance-latest.budget.json`, and `heavy-governance-latest.report.json` under `apps/platform/storage/logs/test-lanes`.
- Rationale: The repository already reads and writes these artifacts. Extending the same contract keeps Spec 209 measurable without introducing a new artifact root or new tool surface.
- Alternatives considered:
- Add a second report directory specifically for Spec 209: rejected because the current lane artifact contract is already canonical.
- Depend only on terminal output: rejected because reviewers need checked-in, inspectable budget evidence.
## Decision 9: Keep author guidance repo-local and adjacent to the existing lane contract
- Decision: Spec 209 should place future heavy-family guidance in the existing test-governance seam, centered on `TestLaneManifest` semantics, guard expectations, and checked-in review guidance rather than creating a separate framework or documentation tree.
- Rationale: Authors and reviewers already need the manifest and guard seams to understand lane ownership. Keeping the guidance there avoids a new abstraction layer and keeps maintenance local to the existing contract.
- Alternatives considered:
- Create a new standalone documentation subsystem for heavy tests: rejected because the guidance is specific to the repository's existing lane contract.
- Leave guidance only in the spec artifacts: rejected because authors need a lasting checked-in hint near the implementation seam after the spec is complete.
## Decision 10: Success requires explicit recovery or explicit recalibration, not quiet tolerance
- Decision: The feature should end in exactly one of two explicit outcomes: the heavy-governance lane recovers within the authoritative threshold for the rollout, which starts at `300` seconds until normalization completes, or the repository documents a conscious recalibration once the honest lane composition and dominant residual costs are understood.
- Rationale: Spec 209 exists to stabilize the heavy lane before CI enforcement. A vague “improved but still heavy” outcome would not satisfy that purpose.
- Alternatives considered:
- Accept any measurable improvement as sufficient: rejected because the spec explicitly requires a budget decision.
- Hard-code recalibration in advance: rejected because the plan must first test whether real recovery is feasible from the dominant hotspot families.

View File

@ -0,0 +1,300 @@
title + explanation + exactly 1 CTA, and tables provide search/sort/filters for core dimensions.
# Feature Specification: Heavy Governance Lane Cost Reduction
**Feature Branch**: `209-heavy-governance-cost`
**Created**: 2026-04-17
**Status**: Draft
**Input**: User description: "Spec 209 - Heavy Governance Lane Cost Reduction"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: The Heavy Governance lane now carries the correct expensive governance families after Spec 208, but the lane itself remains above its documented budget and lacks a precise, shared explanation of which families dominate that cost and why.
- **Today's failure**: Confidence has been repaired, yet Heavy Governance is still too expensive, overbroad family boundaries hide duplicate discovery or validation work, and later CI budget enforcement would punish an honest but still unstable lane.
- **User-visible improvement**: Contributors and reviewers get a Heavy Governance lane whose dominant costs are visible, intentionally sliced, and either brought back under budget or consciously recalibrated with evidence.
- **Smallest enterprise-capable version**: Inventory the dominant heavy-governance families, decompose the top hotspots by trust type and duplicated work, refactor the most overbroad families, rerun the lane with before-and-after reporting, and publish concise author or reviewer guidance for future heavy tests.
- **Explicit non-goals**: No re-run of the lane-segmentation decisions from Spec 208, no general fixture-slimming program, no CI matrix rollout, no browser strategy work, and no blanket removal of legitimate governance coverage.
- **Permanent complexity imported**: A checked-in heavy-family inventory, cost-driver vocabulary, hotspot reporting discipline, budget-recovery decision record, and concise author or reviewer guidance for future heavy tests.
- **Why now**: Spec 208 made the heavy cost honest. Without tightening the Heavy Governance lane now, the next CI-enforcement phase would be built on a lane that is semantically correct but still not economically stable.
- **Why not local**: One-off optimizations cannot explain or control a lane-wide cost class. The repository needs a shared inventory, a shared slimming rule set, and an explicit budget decision that reviewers can evaluate consistently.
- **Approval class**: Cleanup
- **Red flags triggered**: Another governance taxonomy and the possibility of budget recalibration. Defense: the scope is intentionally narrow to repository test-lane cost control and does not introduce new product runtime truth, new product persistence, or new operator-facing surfaces.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: No end-user HTTP routes change. The affected surfaces are repository-owned Heavy Governance lane manifests, hotspot inventories, runtime reports, family-level governance tests, and contributor guidance.
- **Data Ownership**: Workspace-owned classification notes, hotspot evidence, lane-budget reporting, family refactoring rules, and reviewer guidance. No tenant-owned runtime records or new product data are introduced.
- **RBAC**: No end-user authorization behavior changes. The affected actors are repository contributors, reviewers, and maintainers who need an honest and governable Heavy Governance lane.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no new product persistence; only repository-owned inventory, reporting evidence, and guidance updates
- **New abstraction?**: yes, but limited to a repository-level inventory and decomposition model for dominant heavy-governance families
- **New enum/state/reason family?**: no product runtime state is added
- **New cross-domain UI framework/taxonomy?**: no new cross-domain product taxonomy; this work only sharpens the repository's heavy-governance cost vocabulary
- **Current operator problem**: Maintainers cannot tell which heavy-governance families are legitimately expensive, which are redundant, and which are simply overbroad, so the lane stays expensive without a stable correction path.
- **Existing structure is insufficient because**: Spec 208 fixed lane placement, but not family width. The current heavy families can still combine discovery, surface, workflow, and guard trust in ways that repeat work and make budget drift hard to control.
- **Narrowest correct implementation**: Inventory the dominant heavy families, analyze the top hotspots, refactor only the most overbroad families, rerun the lane, and end with an explicit budget-recovery or recalibration decision.
- **Ownership cost**: The team must maintain the hotspot inventory, the family-slimming rules, the before-and-after budget evidence, and the short guidance that keeps future heavy tests from regressing.
- **Alternative intentionally rejected**: Blindly removing assertions or moving families back into lighter lanes, because that would hide cost instead of explaining and controlling it.
- **Release truth**: Current-release repository truth that stabilizes the Heavy Governance lane before any CI budget enforcement is made binding.
## Problem Statement
Spec 208 resolved the earlier dishonesty in lane placement: heavy governance cost is now concentrated in the Heavy Governance lane instead of leaking into Confidence.
That exposed a new and separate problem: the Heavy Governance lane itself is still too expensive for its documented budget.
The cost drivers are now believed to be structural rather than classificatory:
- Some heavy-governance families are too broad and try to prove multiple trust types in one pass.
- Discovery, validation, render, or surface scans may be repeated inside the same family or across closely related families.
- Workflow-heavy and surface-heavy guards may be bundled together even when they should be separate test concerns.
- The most expensive families are now visible, but their internal cost drivers are not yet decomposed well enough to target the right fix.
- The lane is semantically honest but not yet economically stable enough for later CI budget enforcement.
If this remains unresolved, the repository reaches an unhealthy middle state: the right tests live in the right lane, but the lane is still too expensive to use as a dependable budgeted contract.
## Dependencies
- Depends on Spec 206 - Test Suite Governance & Performance Foundation for lane vocabulary, baseline reporting discipline, and budget governance.
- Depends on Spec 207 - Shared Test Fixture Slimming for the earlier reduction of default fixture cost.
- Depends on Spec 208 - Heavy Suite Segmentation for the corrected placement of heavy governance families into the Heavy Governance lane.
- Recommended before any CI matrix or runtime budget enforcement follow-up.
- Does not block ordinary feature delivery, provided new heavy-governance tests continue to follow the current lane-classification rules.
## Goals
- Bring the Heavy Governance lane back under its documented budget or end with an explicit and justified recalibration.
- Inventory the dominant heavy-governance families and their main hotspot files.
- Decompose the top heavy families by trust type, breadth, and duplicate work.
- Reduce redundant discovery, validation, render, or surface work inside the targeted hotspot families.
- Preserve the governance trust delivered by heavy families while making their cost class more controllable and understandable.
- Create a stable basis for later CI budget enforcement.
## Non-Goals
- Reopening the main lane-assignment decisions from Spec 208.
- Replacing the fixture-cost work from Spec 207.
- General CI wiring or hard-fail enforcement.
- Browser-lane optimization.
- Broad rollback of legitimate governance guards merely to improve runtime headlines.
- Primary optimization of Confidence or Fast Feedback beyond any indirect gains that happen naturally.
## Assumptions
- The current Heavy Governance lane already reflects the correct high-level family placement after Spec 208.
- Baseline reporting from the current heavy-governance run can be regenerated before and after this work.
- Some families will remain intentionally expensive, but their purpose and residual cost should still be explicit.
- Not every heavy hotspot will require file splitting; some can be recovered by centralizing repeated work or tightening family scope.
- If a dominant hotspot turns out to be mostly helper-driven or fixture-driven rather than family-breadth-driven, that cause will be recorded explicitly instead of being disguised as a family problem.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Inventory Heavy Governance Hotspots (Priority: P1)
As a maintainer, I want the dominant Heavy Governance families and files explicitly inventoried so I can see which families are making the lane miss its budget and what kind of cost each family represents.
**Why this priority**: Budget recovery cannot be honest until the lane's dominant costs are visible and named.
**Independent Test**: Run the Heavy Governance lane reporting path, review the resulting inventory, and confirm that the dominant families have named purposes, hotspot files, and cost-driver classifications.
**Acceptance Scenarios**:
1. **Given** a current Heavy Governance baseline run, **When** the hotspot inventory is produced, **Then** the dominant families are listed with family name, purpose, hotspot files, and primary cost-driver classification.
2. **Given** a family is known to dominate the lane, **When** the inventory is reviewed, **Then** it is labeled as overbroad, redundant, discovery-heavy, workflow-heavy, surface-heavy, helper-driven, fixture-driven, intentionally heavy, or another explicitly explained cost type.
3. **Given** the lane is over budget, **When** a maintainer reads the inventory, **Then** they can identify which families are responsible for most of the overrun.
---
### User Story 2 - Slim Overbroad Heavy Families Without Losing Trust (Priority: P1)
As a maintainer, I want the most overbroad heavy families split or tightened so the lane stops repeating equivalent discovery or workflow work while keeping governance trust legible.
**Why this priority**: The largest gains are expected to come from fixing the breadth and duplication of the worst hotspot families, not from cosmetic file moves.
**Independent Test**: Refactor one of the top hotspot families, run its focused pack, and confirm that the resulting families have clearer trust boundaries, less repeated work, and preserved guard intent.
**Acceptance Scenarios**:
1. **Given** a heavy family mixes discovery, workflow, and surface trust, **When** it is refactored, **Then** the resulting tests have clearer semantic boundaries and no unnecessary catch-all scope.
2. **Given** duplicate discovery or validation work exists within a targeted family, **When** the family is slimmed, **Then** repeated passes are reduced or centralized without removing the intended governance guard.
3. **Given** a family remains legitimately heavy after refactoring, **When** it is reviewed, **Then** its remaining cost and trust purpose are explicitly documented.
---
### User Story 3 - Resolve Heavy Lane Budget Failure Explicitly (Priority: P2)
As a maintainer, I want the Heavy Governance lane rerun after the slimming pass so I can prove whether the lane is back within budget or whether the budget itself must be consciously recalibrated.
**Why this priority**: The feature is only complete when budget failure ends in a real decision rather than an unexplained acceptance of ongoing overrun.
**Independent Test**: Rerun the lane after the targeted family changes, compare the baseline and post-change results, and confirm that the outcome records either budget recovery or a justified recalibration decision.
**Acceptance Scenarios**:
1. **Given** the top hotspot families have been slimmed, **When** the Heavy Governance lane is rerun, **Then** the before-and-after delta and remaining hotspots are documented.
2. **Given** the lane still exceeds its former budget after real slimming, **When** the results are reviewed, **Then** the outcome records a conscious recalibration decision with evidence instead of silently treating the overrun as acceptable.
3. **Given** a touched heavy-governance family, **When** the post-change manifest and outcome are reviewed, **Then** the family remains in Heavy Governance unless an explicit non-budget rationale and spec-backed reclassification are recorded.
---
### User Story 4 - Guide Future Heavy Test Authors (Priority: P2)
As a reviewer or contributor, I want short guidance for Heavy Governance families so new heavy tests do not reintroduce the same overbroad patterns that made the lane unstable.
**Why this priority**: Without author guidance, the same structural mistakes can reappear immediately after the cleanup.
**Independent Test**: Review the guidance against a new or recently modified heavy-governance test and confirm that an author can decide whether the test belongs in an existing family, needs a new family, or should be split.
**Acceptance Scenarios**:
1. **Given** a new heavy-governance test is proposed, **When** the author guidance is applied, **Then** the reviewer can decide whether it belongs in an existing family or needs a separate family with explicit rationale.
2. **Given** a proposed test mixes discovery, surface, and workflow trust, **When** the guidance is applied, **Then** the reviewer can tell whether those concerns must be split to avoid another catch-all family.
### Edge Cases
- A hotspot family appears expensive, but the dominant cost is actually a shared helper or fixture path outside the family itself.
- Splitting a family creates clearer files but does not reduce runtime because the same central discovery work still runs repeatedly.
- A family is intentionally expensive and should remain in Heavy Governance, but it still needs an explicit explanation so reviewers do not mistake it for accidental bloat.
- The lane remains over budget even after duplicate work is removed, requiring a conscious recalibration rather than more arbitrary trimming.
- Multiple hotspot families depend on the same discovery or validation topology, so the correct optimization is centralization rather than repeated local edits.
## Requirements *(mandatory)*
**Constitution alignment:** This feature changes no end-user routes, no Microsoft Graph behavior, no queued operation semantics, no authorization planes, and no operator-facing product surfaces. It extends repository test-governance rules only, so the heavy-family inventory, slimming rules, hotspot evidence, and budget decision must remain explicit, reviewable, and measurable.
**Constitution alignment (PROP-001 / ABSTR-001 / BLOAT-001 / TEST-TRUTH-001):** The only new structure is a narrow repository-level inventory and decomposition model for heavy-governance hotspots. It is justified because the current over-budget lane cannot be corrected reliably through isolated local edits or by hiding cost in lighter lanes. The solution must stay explicit, avoid speculative frameworking, and preserve clear trust-to-family mapping.
**Budget authority for this rollout:** Until the normalization work is implemented, the Heavy Governance lane summary threshold of `300s` is the authoritative pre-normalization contract for recovery-or-recalibration decisions. The `200s` lane evaluation still emitted by `budgetTargets()` is legacy drift evidence that must remain visible until reconciled, but it is not a second passing threshold.
### Functional Requirements
- **FR-001 Heavy Family Inventory**: The repository MUST produce an explicit inventory of the dominant Heavy Governance families, including family name, primary purpose, primary hotspot files, and primary cost-driver classification.
- **FR-002 Known Hotspot Inclusion**: The inventory MUST include the current top 5 Heavy Governance families by lane time, or enough families to explain at least 80% of the lane's current runtime, whichever set is larger. This inclusion boundary MUST include baseline-profile-start-surfaces, findings-workflow-surfaces, and finding-bulk-actions-workflow while they remain inside that boundary, and MUST expand to any newly discovered family above the same boundary.
- **FR-003 Cost-Driver Classification**: Each inventoried family MUST be classified using a consistent vocabulary that distinguishes at least overbroad, redundant, discovery-heavy, workflow-heavy, surface-heavy, helper-driven, fixture-driven, and intentionally heavy causes.
- **FR-004 Internal Cost Decomposition**: The top hotspot families selected for action MUST each document which breadth is genuinely required, where duplicate discovery or validation work occurs, and which trust types are currently combined.
- **FR-005 No Catch-All Families**: A targeted Heavy Governance family MUST NOT remain a catch-all that mixes unrelated discovery, workflow, and surface trust without explicit documented justification.
- **FR-006 Duplicate Work Reduction**: Where targeted families repeat semantically equivalent discovery, validation, render, or surface-scanning work, that work MUST be reduced, centralized, or otherwise made non-duplicative.
- **FR-007 Guard Preservation**: Every refactored heavy family MUST retain a clear explanation of which governance rule or trust type it protects so that runtime gains do not come from hidden guard loss.
- **FR-008 Hotspot Reporting**: The Heavy Governance reporting output MUST show lane time before and after the slimming pass, top hotspot files or families, remaining open-expensive families, stabilized families, and whether the checked-in hotspot inventory satisfies the top-5-or-80%-of-runtime inclusion rule.
- **FR-009 Honest Budget Outcome**: After the targeted slimming pass, the Heavy Governance lane MUST be rerun and end with one of two explicit outcomes against the authoritative heavy-governance budget contract: documented recovery within the current threshold or a documented recalibration of the heavy-lane budget based on the now-correct lane composition.
- **FR-010 No Cost Hiding**: Heavy-governance families touched by this spec MUST retain Heavy Governance lane membership unless a non-budget rationale for reclassification is explicitly recorded and approved via a spec update. They MUST NOT be moved into lighter lanes solely to satisfy the budget target.
- **FR-011 Residual Cause Recording**: If a hotspot's dominant cost is determined to be primarily fixture-driven, helper-driven, or otherwise outside family scope, that residual cause MUST be recorded explicitly and routed as follow-up debt rather than being misreported as family-width improvement.
- **FR-012 Author And Reviewer Guidance**: The repository MUST provide concise guidance stating when a new heavy family is justified, when a test belongs in an existing heavy family, when a test is too broad, and when discovery, surface, and workflow concerns should be separated.
- **FR-013 Budget Contract Precedence**: For Spec 209 acceptance and reporting, the Heavy Governance lane summary threshold of `300s` is the authoritative pre-normalization budget contract until the normalization work publishes one reconciled threshold. The `200s` value still emitted by `budgetTargets()` MUST remain visible as legacy drift evidence but MUST NOT be interpreted as a second passing threshold.
### Non-Functional Requirements
- **NFR-001 Budget Honesty**: Heavy Governance cost must remain visible and attributable rather than being hidden through relabeling or silent exclusions.
- **NFR-002 Review Clarity**: A reviewer must be able to explain why a dominant family is heavy and what trust it delivers without relying on local tribal knowledge.
- **NFR-003 Incremental Slimming**: The highest-cost families must be reducible in targeted slices rather than requiring a full rewrite of the lane.
- **NFR-004 Stable Enforcement Readiness**: The resulting lane must be stable enough that later CI budget enforcement can treat its budget as a credible contract, meaning a heavy-governance rerun through the standard wrappers emits summary, budget, and report artifacts that expose the same authoritative threshold and the same budget outcome classification.
## Work Packages
### Work Package A - Heavy Hotspot Inventory
- Profile the current Heavy Governance lane.
- Identify the dominant families and hotspot files.
- Classify each dominant family by primary cost driver.
- Record the current heavy-lane baseline that the recovery pass will be measured against.
### Work Package B - Family Semantics Audit
- For each top hotspot family, identify which trust type it delivers.
- Separate required breadth from accidental breadth.
- Identify duplicate discovery, validation, or surface work.
- Distinguish true family-width issues from helper or fixture issues.
### Work Package C - Dominant Family Refactoring
- Split overbroad families into narrower units when needed.
- Centralize repeated discovery or validation work when that is the real hotspot.
- Separate workflow-heavy checks from surface-heavy or discovery-heavy guards where that improves both clarity and cost.
- Keep each resulting family's trust purpose explicit.
### Work Package D - Budget Recovery Validation
- Rerun the Heavy Governance lane after the slimming pass.
- Document the before-and-after lane delta and remaining hotspots.
- Decide explicitly whether the lane is back under budget or whether the budget must be recalibrated.
- Record any bounded residual debt that remains after honest slimming.
### Work Package E - Author And Reviewer Guidance
- Add short rules for when a heavy family is justified.
- Explain when a new test belongs in an existing heavy family.
- Explain when a proposed heavy test is too broad.
- Give reviewers a clear rule for splitting discovery, workflow, and surface trust when they are unnecessarily combined.
## Deliverables
- A checked-in inventory of dominant Heavy Governance families and hotspot files.
- A cost-driver classification for the heavy-lane hotspots.
- Slimmed or decomposed versions of the dominant overbroad families.
- Updated before-and-after heavy-lane reporting and budget evidence.
- An explicit budget outcome: recovery within the current threshold or a consciously documented recalibration.
- Short author or reviewer guidance for future Heavy Governance tests.
- A final summary of residual risks or remaining debt.
## Risks
### Governance Hollowing
Over-aggressive slimming could remove real governance trust instead of only removing redundant work.
### Cosmetic Decomposition
Renaming or splitting files without actually reducing duplicate work could make the lane look cleaner without recovering budget.
### Budget Gaming
Moving heavy cost into lighter lanes or redefining scope to satisfy the target would damage the credibility of the repository's lane governance.
### Misclassified Residual Cost
Some hotspots may ultimately be helper-driven or fixture-driven; if that is not recorded honestly, the family-level analysis will be misleading.
### Overfitting To Today's Hotspots
If the spec focuses only on today's top families and does not leave reviewer guidance behind, the next wave of heavy tests can recreate the same problem.
## Rollout Guidance
- Profile the current lane before making structural edits.
- Audit the top families semantically before trimming any assertions.
- Prefer removing duplicate work or clarifying family scope before deleting coverage.
- Rerun the lane after the targeted slimming pass and record the delta immediately.
- If the lane is still honestly over budget, end with a conscious recalibration or bounded follow-up debt instead of silent acceptance.
## Design Rules
- **Heavy stays honest**: Heavy cost must remain visible in the Heavy Governance lane.
- **Trust first, then trimming**: Protect the governance rule first, then remove accidental breadth or duplication.
- **No catch-all families**: A family should not mix unrelated trust types without explicit reason.
- **No duplicate discovery without cause**: Equivalent discovery or validation passes require a clear justification.
- **Budget failure resolves explicitly**: Over-budget status must end in recovery or a conscious recalibration decision.
- **Reviewer legibility is required**: A reviewer must be able to see why a family is heavy and what it protects.
### Key Entities *(include if feature involves data)*
- **Heavy Governance Family**: A named cluster of governance tests that contributes deliberate high-cost safety to the Heavy Governance lane.
- **Cost-Driver Classification**: The recorded explanation of why a heavy family is expensive, such as overbroad scope, duplicate work, discovery breadth, workflow breadth, or intentionally retained depth.
- **Hotspot Inventory**: The checked-in record of dominant heavy-lane families, their purpose, hotspot files, and primary cost drivers.
- **Budget Outcome Record**: The documented result that states whether the Heavy Governance lane recovered within budget or required a conscious recalibration.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: The hotspot inventory covers at least the top 5 Heavy Governance families by current lane time, or enough families to explain at least 80% of the lane's current runtime, whichever set is larger.
- **SC-002**: Every targeted hotspot family has a documented purpose, primary trust type, hotspot files, and primary cost-driver classification.
- **SC-003**: At least the top 3 targeted hotspot families are slimmed, decomposed, or explicitly retained as intentionally heavy with a documented reason.
- **SC-004**: After the slimming pass, the Heavy Governance lane either runs within the authoritative threshold defined for this rollout, which is `300s` until the normalization work publishes a reconciled threshold, or ends with a newly documented budget that includes evidence that the lane composition is now semantically correct and the remaining cost is legitimate.
- **SC-005**: The post-change reporting view exposes at least the top 10 Heavy Governance hotspots, confirms that the inventory covers the top 5 families or at least 80% of runtime, whichever is larger, and marks which targeted families improved, which remain open, and which were intentionally retained as heavy.
- **SC-006**: Reviewer guidance enables a contributor to classify a new heavy-governance test without undocumented local knowledge and to detect when a proposed test is too broad.
- **SC-007**: No targeted heavy-governance family is moved into Fast Feedback or Confidence solely for budget reasons during this rollout.

View File

@ -0,0 +1,182 @@
# Tasks: Heavy Governance Lane Cost Reduction
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/209-heavy-governance-cost/`
**Prerequisites**: `plan.md` (required), `spec.md` (required), `research.md`, `data-model.md`, `contracts/`, `quickstart.md`
**Tests**: Required. This feature changes repository test-governance behavior, so each user story includes Pest guard coverage and focused validation through Sail.
**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently.
## Phase 1: Setup (Shared Context)
**Purpose**: Refresh and confirm the current heavy-governance budget inputs, artifact contract, and hotspot families before changing shared seams.
- [X] T001 Refresh the current heavy-governance baseline by rerunning `scripts/platform-test-lane` and `scripts/platform-test-report` before family edits, then audit the refreshed artifacts in `apps/platform/storage/logs/test-lanes/heavy-governance-latest.summary.md`, `apps/platform/storage/logs/test-lanes/heavy-governance-latest.budget.json`, and `apps/platform/storage/logs/test-lanes/heavy-governance-latest.report.json`
- [X] T002 [P] Review the current heavy-lane seams in `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Support/TestLaneBudget.php`, `apps/platform/tests/Support/TestLaneReport.php`, `scripts/platform-test-lane`, and `scripts/platform-test-report`
- [X] T003 [P] Review the primary hotspot families in `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineActionAuthorizationTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowViewActionsTest.php`, and `apps/platform/tests/Feature/Findings/FindingBulkActionsTest.php`
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Add the shared manifest, budget, report, and wrapper seams that all user stories depend on.
**Critical**: No user story work should begin until this phase is complete.
- [X] T004 Extend `apps/platform/tests/Support/TestLaneManifest.php` with neutral accessors for the heavy-governance budget contract, hotspot inventory records, decomposition records, slimming decisions, and budget outcome placeholders aligned to `specs/209-heavy-governance-cost/contracts/heavy-governance-hotspot-inventory.schema.json`
- [X] T005 [P] Extend `apps/platform/tests/Support/TestLaneBudget.php` and `apps/platform/tests/Support/TestLaneReport.php` with neutral helpers for normalized heavy-governance thresholds, snapshot comparison, and explicit recovered or recalibrated outcomes
- [X] T006 [P] Update `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php`, `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php`, and `apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php` to accept the expanded heavy-governance inventory, snapshot, and budget outcome metadata shape
- [X] T007 [P] Update `scripts/platform-test-lane` and `scripts/platform-test-report` so heavy-governance baseline and rerun artifacts can be refreshed consistently under `apps/platform/storage/logs/test-lanes`
**Checkpoint**: Shared heavy-governance seams are ready for story-specific implementation.
---
## Phase 3: User Story 1 - Inventory Heavy Governance Hotspots (Priority: P1)
**Goal**: Seed a checked-in hotspot inventory that names the dominant heavy-governance families, their measured cost, and the authoritative pre-normalization budget contract plus legacy drift signal that must be normalized.
**Independent Test**: Run the heavy-governance report path and confirm the manifest and artifacts expose the dominant families, their cost-driver categories, and the current budget signals.
### Tests for User Story 1
- [X] T008 [P] [US1] Expand `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` to assert the top heavy-governance families, priority tiers, the authoritative pre-normalization `300s` threshold, the visible legacy `200s` drift signal, and lane-membership invariants for touched families are exposed from `apps/platform/tests/Support/TestLaneManifest.php`
- [X] T009 [P] [US1] Expand `apps/platform/tests/Feature/Guards/ProfileLaneContractTest.php` and `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` to assert heavy-governance reports surface top 10 hotspots, family totals, classification totals, inventory coverage for the top 5 families or at least 80% of runtime, whichever is larger, and snapshot artifact paths under `apps/platform/storage/logs/test-lanes`
### Implementation for User Story 1
- [X] T010 [US1] Populate the canonical hotspot inventory and current heavy-governance budget contract in `apps/platform/tests/Support/TestLaneManifest.php` for the current top 5 heavy families by runtime, or enough families to explain at least 80% of runtime, whichever set is larger, including `baseline-profile-start-surfaces`, `findings-workflow-surfaces`, `finding-bulk-actions-workflow`, `action-surface-contract`, `ops-ux-governance`, and `workspace-settings-slice-management`
- [X] T011 [US1] Extend `apps/platform/tests/Support/TestLaneReport.php` to emit coverage-oriented heavy-governance snapshot data that explains the top 5 families or at least 80% of lane runtime, whichever is larger, and reports whether the inclusion rule is satisfied
- [X] T012 [US1] Align the measured hotspot baseline and reviewer-facing inventory summary in `specs/209-heavy-governance-cost/quickstart.md` with the canonical inventory stored in `apps/platform/tests/Support/TestLaneManifest.php`, including the authoritative pre-normalization `300s` threshold and the legacy `200s` drift signal
**Checkpoint**: User Story 1 is complete when the dominant heavy-governance hotspots are reviewable and contract-tested without yet changing the family internals.
---
## Phase 4: User Story 2 - Slim Overbroad Heavy Families Without Losing Trust (Priority: P1)
**Goal**: Reduce duplicated work and accidental breadth in the dominant heavy families while preserving their governance guarantees.
**Independent Test**: Refactor the targeted hotspot families, run the focused packs, and confirm the resulting families keep their guard intent while shedding repeated work.
### Tests for User Story 2
- [X] T013 [P] [US2] Expand focused regression coverage in `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, and `apps/platform/tests/Feature/Filament/BaselineActionAuthorizationTest.php` to lock baseline start-surface guard behavior before slimming
- [X] T014 [P] [US2] Expand focused regression coverage in `apps/platform/tests/Feature/Findings/FindingBulkActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowViewActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingsListFiltersTest.php`, and `apps/platform/tests/Feature/Findings/FindingExceptionRenewalTest.php` to lock workflow, filter, and audit behavior before decomposition
- [X] T015 [P] [US2] Expand `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php` and `apps/platform/tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php` to preserve current governance invariants if second-wave surface-guard slimming becomes necessary
### Implementation for User Story 2
- [X] T016 [US2] Decompose the `baseline-profile-start-surfaces` family across `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, and `apps/platform/tests/Feature/Filament/BaselineActionAuthorizationTest.php` to remove repeated Livewire mounts and gating matrices while preserving authorization and rollout guards
- [X] T017 [P] [US2] Slim the `findings-workflow-surfaces` family in `apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowViewActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingsListFiltersTest.php`, and `apps/platform/tests/Feature/Findings/FindingExceptionRenewalTest.php` by separating required workflow trust from repeated surface or filter work
- [X] T018 [P] [US2] Slim the `finding-bulk-actions-workflow` family in `apps/platform/tests/Feature/Findings/FindingBulkActionsTest.php` to remove duplicated bulk-action setup and audit fan-out without weakening per-record workflow guarantees
- [X] T019 [US2] Record decomposition results, residual causes, and slimming decisions for the workflow-heavy hotspot families in `apps/platform/tests/Support/TestLaneManifest.php`
- [X] T020 [US2] Audit `apps/platform/tests/Feature/Guards/ActionSurfaceContractTest.php`, `apps/platform/tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php`, `apps/platform/tests/Feature/OpsUx/OperationCatalogCoverageTest.php`, `apps/platform/tests/Feature/OpsUx/OperateHubShellTest.php`, and `apps/platform/tests/Feature/OpsUx/ActiveRunsTest.php` to either identify removable duplication or explicitly retain them as intentionally heavy in `apps/platform/tests/Support/TestLaneManifest.php`
**Checkpoint**: User Story 2 is complete when the primary workflow-heavy hotspot families are slimmer, their trust boundaries are explicit, and any intentionally retained heavy families are documented as such.
---
## Phase 5: User Story 3 - Resolve Heavy Lane Budget Failure Explicitly (Priority: P2)
**Goal**: Normalize the heavy-governance budget contract, rerun the lane, and end with an explicit recovered or recalibrated outcome.
**Independent Test**: Rerun the heavy-governance lane after the slimming pass and confirm the updated artifacts expose one authoritative threshold, before-and-after deltas, and an explicit budget decision.
### Tests for User Story 3
- [X] T021 [P] [US3] Expand `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` and `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php` to assert one authoritative heavy-governance threshold, explicit budget decision status, and remaining-open-family reporting
- [X] T022 [P] [US3] Expand `apps/platform/tests/Feature/Guards/ProfileLaneContractTest.php` and `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` to assert baseline and post-change heavy-governance snapshots plus final budget outcome artifacts under `apps/platform/storage/logs/test-lanes`, including non-conflicting threshold and outcome values across summary, budget, and report outputs
### Implementation for User Story 3
- [X] T023 [US3] Normalize the heavy-governance budget contract in `apps/platform/tests/Support/TestLaneBudget.php` and `apps/platform/tests/Support/TestLaneManifest.php` so the authoritative pre-normalization `300s` summary threshold and the legacy `200s` detailed budget evaluation resolve to one authoritative threshold and lifecycle state
- [X] T024 [US3] Extend `apps/platform/tests/Support/TestLaneReport.php` to emit baseline or post-change deltas, remaining open families, and explicit `recovered` or `recalibrated` budget outcome records
- [X] T025 [US3] Audit `apps/platform/tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php` as the first fallback ui-workflow hotspot and either narrow duplicated work or record it as retained residual cost in `apps/platform/tests/Support/TestLaneManifest.php`
- [X] T026 [US3] Update `scripts/platform-test-lane` and `scripts/platform-test-report` so heavy-governance baseline capture and rerun reporting produce consistent summary, budget, and report artifacts for the final decision
- [X] T027 [US3] Refresh `apps/platform/storage/logs/test-lanes/heavy-governance-latest.summary.md`, `apps/platform/storage/logs/test-lanes/heavy-governance-latest.budget.json`, and `apps/platform/storage/logs/test-lanes/heavy-governance-latest.report.json` by rerunning the heavy-governance lane after the slimming pass and confirming the refreshed artifacts agree on the final threshold and budget outcome classification
- [X] T028 [US3] Record the final heavy-governance recovery or recalibration rationale in `apps/platform/tests/Support/TestLaneManifest.php` and `specs/209-heavy-governance-cost/quickstart.md`
**Checkpoint**: User Story 3 is complete when the heavy-governance lane exposes one authoritative budget contract and a final explicit budget decision backed by refreshed artifacts.
---
## Phase 6: User Story 4 - Guide Future Heavy Test Authors (Priority: P2)
**Goal**: Leave behind short, enforceable rules that stop new heavy-governance tests from recreating the same overbroad patterns.
**Independent Test**: Review the manifest guidance and taxonomy guards to confirm a reviewer can decide whether a new heavy test belongs in an existing family, needs a new family, or must be split.
### Tests for User Story 4
- [X] T029 [P] [US4] Expand `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php` to assert the heavy author-guidance rules cover new-family creation, family reuse, overbroad tests, and separation of discovery, workflow, and surface trust
- [X] T030 [P] [US4] Expand `apps/platform/tests/Feature/Guards/TestTaxonomyPlacementGuardTest.php` and `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` to require a valid family or guidance-backed classification decision for future heavy-governance additions
### Implementation for User Story 4
- [X] T031 [US4] Add canonical heavy-author guidance rules and anti-pattern signals to `apps/platform/tests/Support/TestLaneManifest.php`
- [X] T032 [US4] Publish reviewer-facing heavy-family guidance and residual-debt rules in `specs/209-heavy-governance-cost/quickstart.md` and keep the canonical wording synchronized with `apps/platform/tests/Support/TestLaneManifest.php`
**Checkpoint**: User Story 4 is complete when future heavy-governance additions have explicit guidance and guard-backed review rules.
---
## Phase 7: Polish & Cross-Cutting Concerns
**Purpose**: Validate the end-to-end rollout, format the code, and remove superseded notes after the heavy-lane decision is settled.
- [X] T033 Run focused Pest coverage for `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php`, `apps/platform/tests/Feature/Filament/BaselineActionAuthorizationTest.php`, `apps/platform/tests/Feature/Findings/FindingBulkActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowRowActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingWorkflowViewActionsTest.php`, `apps/platform/tests/Feature/Findings/FindingsListFiltersTest.php`, `apps/platform/tests/Feature/Findings/FindingExceptionRenewalTest.php`, and `apps/platform/tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php` with `cd apps/platform && ./vendor/bin/sail artisan test --compact ...`
- [X] T034 Run heavy-governance wrapper validation via `scripts/platform-test-lane`, `scripts/platform-test-report`, and inspect `apps/platform/storage/logs/test-lanes/heavy-governance-latest.summary.md`, `apps/platform/storage/logs/test-lanes/heavy-governance-latest.budget.json`, and `apps/platform/storage/logs/test-lanes/heavy-governance-latest.report.json` to confirm the final budget outcome and a non-conflicting threshold or outcome contract across all three artifacts
- [X] T035 Run formatting for `apps/platform/` with `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
- [X] T036 Remove stale dual-threshold comments, superseded family notes, and obsolete draft placeholders in `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Support/TestLaneBudget.php`, `apps/platform/tests/Support/TestLaneReport.php`, and `specs/209-heavy-governance-cost/quickstart.md`
---
## Dependencies
- Phase 1 must complete before Phase 2.
- Phase 2 must complete before any user story work begins.
- User Story 1 is the MVP and must complete before User Story 2, User Story 3, or User Story 4.
- User Story 2 depends on User Story 1 because slimming decisions require the canonical hotspot inventory and decomposition scaffolding.
- User Story 3 depends on User Story 1 and User Story 2 because the explicit budget decision depends on the updated inventory and the slimming pass.
- User Story 4 depends on User Story 1 and User Story 3 because the final guidance should reflect the authoritative family model and the settled budget contract.
- Phase 7 depends on all user stories.
## Parallel Execution Examples
### User Story 1
- Run T008 and T009 in parallel to lock the inventory and artifact-report contracts.
- After T010 lands, T011 and T012 can proceed in parallel across report output and reviewer-facing guidance.
### User Story 2
- Run T013, T014, and T015 in parallel because they lock separate hotspot families before slimming.
- After T016 lands, T017 and T018 can run in parallel across findings workflow and bulk-action families.
### User Story 3
- Run T021 and T022 in parallel because they cover budget-contract and artifact-outcome guards separately.
- After T023 lands, T024 and T026 can run in parallel across report generation and wrapper reporting.
### User Story 4
- Run T029 and T030 in parallel because they enforce different review surfaces.
- After T031 lands, T032 can publish the final wording in the quickstart guidance.
## Implementation Strategy
### MVP First
- Deliver User Story 1 first to establish the canonical hotspot inventory and the current heavy-governance budget signal.
- Deliver User Story 2 next to produce the first real runtime recovery by narrowing the dominant workflow-heavy families.
### Incremental Delivery
- Add User Story 3 after the primary slimming pass so the budget decision reflects real runtime changes rather than a draft threshold.
- Add User Story 4 last so the guidance codifies the final family model and budget contract.
### Validation Sequence
- Use focused hotspot tests first, then guard suites, then heavy-governance wrapper reruns, and only then refresh formatting and stale-note cleanup.
- Treat `apps/platform/storage/logs/test-lanes/` as the canonical artifact root throughout validation and budget review.

View File

@ -0,0 +1,39 @@
# Specification Quality Checklist: CI Test Matrix & Runtime Budget Enforcement
**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-04-17
**Feature**: [spec.md](../spec.md)
## Content Quality
- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed
## Requirement Completeness
- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified
## Feature Readiness
- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification
## Notes
- Validation run: 2026-04-17
- No template placeholders or [NEEDS CLARIFICATION] markers remain.
- The spec stays repository-governance-focused: it defines trigger policy, budget semantics, artifact expectations, and contributor behavior without prescribing language-, framework-, or API-level implementation.
- CI-specific nouns such as lane, artifact, budget, and failure class are treated as domain requirements for the repository validation contract rather than low-level implementation detail.
- The scope remains intentionally narrow: it operationalizes the existing governance work from Specs 206 through 209 instead of inventing a second test-execution model.
- Items marked incomplete require spec updates before `/speckit.clarify` or `/speckit.plan`.

View File

@ -0,0 +1,317 @@
openapi: 3.1.0
info:
title: CI Lane Governance Logical Contract
version: 1.0.0
description: |
Logical contract for Spec 210. This is not a public runtime API.
It documents the semantics that checked-in Gitea workflows, repo-root wrappers,
and test-governance support classes must satisfy together.
paths:
/logical/ci/workflows/{workflowId}/execute:
post:
summary: Execute one governed CI workflow path
operationId: executeWorkflowProfile
parameters:
- name: workflowId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowExecutionRequest'
responses:
'200':
description: Workflow execution plan resolved
content:
application/json:
schema:
$ref: '#/components/schemas/WorkflowExecutionResult'
/logical/ci/lanes/{laneId}/evaluate-budget:
post:
summary: Evaluate one lane budget under a trigger-specific CI policy
operationId: evaluateLaneBudget
parameters:
- name: laneId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BudgetEvaluationRequest'
responses:
'200':
description: Budget evaluation returned
content:
application/json:
schema:
$ref: '#/components/schemas/BudgetEvaluationResult'
/logical/ci/lanes/{laneId}/stage-artifacts:
post:
summary: Stage lane artifacts into a deterministic upload directory
operationId: stageLaneArtifacts
parameters:
- name: laneId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ArtifactStagingRequest'
responses:
'200':
description: Artifact staging completed
content:
application/json:
schema:
$ref: '#/components/schemas/ArtifactStagingResult'
/logical/ci/runs/{runId}/summary:
get:
summary: Read the normalized CI run summary for one governed lane execution
operationId: readRunSummary
parameters:
- name: runId
in: path
required: true
schema:
type: string
responses:
'200':
description: Run summary returned
content:
application/json:
schema:
$ref: '#/components/schemas/CiRunSummary'
components:
schemas:
WorkflowExecutionRequest:
type: object
additionalProperties: false
required:
- triggerClass
- gitRef
- runnerLabel
properties:
triggerClass:
type: string
enum:
- pull-request
- mainline-push
- scheduled
- manual
gitRef:
type: string
runnerLabel:
type: string
requestedLanes:
type: array
items:
type: string
WorkflowExecutionResult:
type: object
additionalProperties: false
required:
- workflowId
- laneExecutions
properties:
workflowId:
type: string
laneExecutions:
type: array
items:
$ref: '#/components/schemas/LaneExecutionPlan'
LaneExecutionPlan:
type: object
additionalProperties: false
required:
- laneId
- executionWrapper
- requiredArtifacts
- budgetPolicy
properties:
laneId:
type: string
executionWrapper:
type: string
reportWrapper:
type: string
requiredArtifacts:
type: array
items:
type: string
budgetPolicy:
$ref: '#/components/schemas/BudgetPolicy'
BudgetPolicy:
type: object
additionalProperties: false
required:
- thresholdSource
- effectiveThresholdSeconds
- enforcementMode
properties:
thresholdSource:
type: string
enum:
- lane-budget
- governance-contract
effectiveThresholdSeconds:
type: number
varianceAllowanceSeconds:
type: number
enforcementMode:
type: string
enum:
- hard-fail
- soft-warn
- trend-only
lifecycleState:
type: string
BudgetEvaluationRequest:
type: object
additionalProperties: false
required:
- triggerClass
- measuredSeconds
properties:
triggerClass:
type: string
measuredSeconds:
type: number
BudgetEvaluationResult:
type: object
additionalProperties: false
required:
- laneId
- budgetStatus
- blockingStatus
properties:
laneId:
type: string
budgetStatus:
type: string
enum:
- within-budget
- warning
- over-budget
blockingStatus:
type: string
enum:
- blocking
- non-blocking-warning
- informational
primaryFailureClassId:
type:
- string
- 'null'
ArtifactStagingRequest:
type: object
additionalProperties: false
required:
- workflowId
- laneId
- sourceDirectory
- stagingDirectory
properties:
workflowId:
type: string
laneId:
type: string
sourceDirectory:
type: string
stagingDirectory:
type: string
sourcePatterns:
type: array
items:
type: string
ArtifactStagingResult:
type: object
additionalProperties: false
required:
- laneId
- stagedArtifacts
- complete
properties:
laneId:
type: string
stagedArtifacts:
type: array
items:
$ref: '#/components/schemas/ArtifactRecord'
complete:
type: boolean
primaryFailureClassId:
type:
- string
- 'null'
ArtifactRecord:
type: object
additionalProperties: false
required:
- artifactType
- relativePath
properties:
artifactType:
type: string
relativePath:
type: string
required:
type: boolean
CiRunSummary:
type: object
additionalProperties: false
required:
- runId
- workflowId
- laneId
- testStatus
- artifactStatus
- blockingStatus
properties:
runId:
type: string
workflowId:
type: string
laneId:
type: string
testStatus:
type: string
enum:
- passed
- failed
artifactStatus:
type: string
enum:
- complete
- incomplete
budgetStatus:
type: string
enum:
- within-budget
- warning
- over-budget
blockingStatus:
type: string
enum:
- blocking
- non-blocking-warning
- informational
primaryFailureClassId:
type:
- string
- 'null'
publishedArtifacts:
type: array
items:
$ref: '#/components/schemas/ArtifactRecord'

View File

@ -0,0 +1,383 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://tenantpilot.local/specs/210/contracts/ci-lane-matrix.schema.json",
"title": "CI Lane Matrix Contract",
"type": "object",
"additionalProperties": false,
"required": [
"version",
"mainlineBranch",
"workflowProfiles",
"laneBindings",
"budgetEnforcementProfiles",
"failureClasses"
],
"properties": {
"version": {
"type": "integer",
"minimum": 1
},
"mainlineBranch": {
"type": "string",
"minLength": 1
},
"workflowProfiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/workflowProfile"
}
},
"laneBindings": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/laneBinding"
}
},
"budgetEnforcementProfiles": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/budgetEnforcementProfile"
}
},
"artifactPublicationContracts": {
"type": "array",
"items": {
"$ref": "#/definitions/artifactPublicationContract"
}
},
"failureClasses": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/failureClassification"
}
}
},
"definitions": {
"workflowProfile": {
"type": "object",
"additionalProperties": false,
"required": [
"workflowId",
"filePath",
"triggerClass",
"gitEvents",
"runnerLabel",
"blockingDefault",
"laneBindings"
],
"properties": {
"workflowId": {
"type": "string",
"minLength": 1
},
"filePath": {
"type": "string",
"pattern": "^\\.gitea/workflows/.+\\.ya?ml$"
},
"triggerClass": {
"enum": [
"pull-request",
"mainline-push",
"scheduled",
"manual"
]
},
"gitEvents": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"branchFilters": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"runnerLabel": {
"type": "string",
"minLength": 1
},
"blockingDefault": {
"type": "boolean"
},
"scheduleCron": {
"type": [
"string",
"null"
]
},
"laneBindings": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}
}
},
"laneBinding": {
"type": "object",
"additionalProperties": false,
"required": [
"laneId",
"executionWrapper",
"reportWrapper",
"commandRef",
"governanceClass",
"requiredArtifacts"
],
"properties": {
"laneId": {
"enum": [
"fast-feedback",
"confidence",
"heavy-governance",
"browser",
"profiling",
"junit"
]
},
"executionWrapper": {
"const": "scripts/platform-test-lane"
},
"reportWrapper": {
"const": "scripts/platform-test-report"
},
"commandRef": {
"type": "string",
"minLength": 1
},
"governanceClass": {
"enum": [
"fast",
"confidence",
"heavy",
"support"
]
},
"parallelMode": {
"enum": [
"required",
"optional",
"forbidden"
]
},
"requiredArtifacts": {
"type": "array",
"minItems": 1,
"items": {
"enum": [
"summary.md",
"budget.json",
"report.json",
"junit.xml",
"profile.txt"
]
}
},
"optionalArtifacts": {
"type": "array",
"items": {
"enum": [
"summary.md",
"budget.json",
"report.json",
"junit.xml",
"profile.txt"
]
}
},
"artifactExportProfile": {
"type": "string",
"minLength": 1
}
}
},
"budgetEnforcementProfile": {
"type": "object",
"additionalProperties": false,
"required": [
"policyId",
"laneId",
"triggerClass",
"thresholdSource",
"baseThresholdSeconds",
"varianceAllowanceSeconds",
"effectiveThresholdSeconds",
"enforcementMode",
"lifecycleState"
],
"properties": {
"policyId": {
"type": "string",
"minLength": 1
},
"laneId": {
"type": "string",
"minLength": 1
},
"triggerClass": {
"enum": [
"pull-request",
"mainline-push",
"scheduled",
"manual"
]
},
"thresholdSource": {
"enum": [
"lane-budget",
"governance-contract"
]
},
"baseThresholdSeconds": {
"type": "number",
"minimum": 0
},
"varianceAllowanceSeconds": {
"type": "number",
"minimum": 0
},
"effectiveThresholdSeconds": {
"type": "number",
"minimum": 0
},
"enforcementMode": {
"enum": [
"hard-fail",
"soft-warn",
"trend-only"
]
},
"lifecycleState": {
"type": "string",
"minLength": 1
},
"reviewCadence": {
"type": "string"
}
}
},
"artifactPublicationContract": {
"type": "object",
"additionalProperties": false,
"required": [
"contractId",
"laneId",
"sourceDirectory",
"requiredFiles",
"stagingDirectory",
"stagedNamePattern",
"uploadGroupName"
],
"properties": {
"contractId": {
"type": "string",
"minLength": 1
},
"laneId": {
"type": "string",
"minLength": 1
},
"sourceDirectory": {
"const": "storage/logs/test-lanes"
},
"sourcePatterns": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"requiredFiles": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
},
"optionalFiles": {
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
},
"stagingDirectory": {
"type": "string",
"minLength": 1
},
"stagedNamePattern": {
"type": "string",
"minLength": 1
},
"uploadGroupName": {
"type": "string",
"minLength": 1
},
"retentionClass": {
"enum": [
"pr-short",
"mainline-medium",
"scheduled-medium"
]
}
}
},
"failureClassification": {
"type": "object",
"additionalProperties": false,
"required": [
"failureClassId",
"sourceStep",
"blockingOn",
"summaryLabel",
"remediationHint"
],
"properties": {
"failureClassId": {
"enum": [
"test-failure",
"wrapper-failure",
"budget-breach",
"artifact-publication-failure",
"infrastructure-failure"
]
},
"sourceStep": {
"type": "string",
"minLength": 1
},
"blockingOn": {
"type": "array",
"items": {
"enum": [
"pull-request",
"mainline-push",
"scheduled",
"manual"
]
}
},
"summaryLabel": {
"type": "string",
"minLength": 1
},
"remediationHint": {
"type": "string",
"minLength": 1
}
}
}
}
}

View File

@ -0,0 +1,173 @@
# Data Model: CI Test Matrix & Runtime Budget Enforcement
## Model Scope
This feature adds no product database tables. The model below describes checked-in repository governance objects that live in workflow YAML, repo-root scripts, and the existing `TestLaneManifest` / `TestLaneBudget` / `TestLaneReport` seams.
## 1. WorkflowProfile
Represents one checked-in Gitea workflow file that owns a single trigger class.
| Field | Type | Description |
|---|---|---|
| `workflowId` | string | Stable identifier such as `pr-fast-feedback` or `main-confidence`. |
| `filePath` | string | Checked-in workflow path under `.gitea/workflows/`. |
| `triggerClass` | enum | One of `pull-request`, `mainline-push`, `scheduled`, `manual`. |
| `gitEvents` | list<string> | Concrete Gitea events that activate the workflow. |
| `branchFilters` | list<string> | Branch list or schedule scope, such as `dev`. |
| `runnerLabel` | string | Single runner label used by the workflow. |
| `blockingDefault` | boolean | Whether this workflow blocks the associated shared path by default. |
| `scheduleCron` | string or null | Cron expression for scheduled workflows. |
| `laneBindings` | list<string> | Ordered lane ids executed by the workflow. |
**Validation rules**
- One `WorkflowProfile` owns exactly one trigger class.
- Pull request workflows cannot bind `heavy-governance` or `browser`.
- The mainline workflow targets `dev`.
- Scheduled and manual workflows must remain explicit about which heavy lane they own.
## 2. LaneBinding
Represents the workflow-to-lane mapping for an existing `TestLaneManifest` lane.
| Field | Type | Description |
|---|---|---|
| `laneId` | enum | Existing manifest lane id: `fast-feedback`, `confidence`, `heavy-governance`, `browser`, `profiling`, or `junit`. |
| `executionWrapper` | string | Repo-root wrapper path, expected to be `scripts/platform-test-lane`. |
| `reportWrapper` | string | Repo-root report wrapper path, expected to be `scripts/platform-test-report`. |
| `commandRef` | string | Existing manifest command reference such as `test` or `test:confidence`. |
| `governanceClass` | enum | `fast`, `confidence`, `heavy`, or `support`. |
| `parallelMode` | enum | `required`, `optional`, or `forbidden`, inherited from the manifest. |
| `requiredArtifacts` | list<string> | Artifact types that must exist before upload. |
| `optionalArtifacts` | list<string> | Artifact types that may exist for this lane only. |
| `artifactExportProfile` | string | Stable export profile used by the staging helper. |
**Validation rules**
- `laneId` must exist in `TestLaneManifest::lanes()`.
- Workflows may not inline selectors or test paths that bypass the lane binding.
- Confidence and Fast Feedback must both require JUnit, summary, budget, and report artifacts.
- `profiling` may remain unbound from default CI workflows while still existing in the model.
## 3. BudgetEnforcementProfile
Represents how one lane budget behaves for one trigger class.
| Field | Type | Description |
|---|---|---|
| `policyId` | string | Stable identifier such as `pr-fast-feedback-budget`. |
| `laneId` | string | Lane governed by the policy. |
| `triggerClass` | string | Trigger class where the policy applies. |
| `thresholdSource` | enum | `lane-budget` or `governance-contract`. |
| `baseThresholdSeconds` | number | Threshold before CI tolerance is applied. |
| `varianceAllowanceSeconds` | number | Tolerance added for CI runner variance. |
| `effectiveThresholdSeconds` | number | Threshold used to classify the run. |
| `enforcementMode` | enum | `hard-fail`, `soft-warn`, or `trend-only`. |
| `lifecycleState` | string | Existing maturity state such as `documented`, `draft`, or `recalibrated`. |
| `reviewCadence` | string | Human review cadence for re-evaluating the policy. |
**Validation rules**
- `hard-fail` requires a documented tolerance strategy.
- `governance-contract` is reserved for `heavy-governance`.
- `soft-warn` or `trend-only` are the default for immature or scheduled lanes.
**State transitions**
- `within-budget``warning``over-budget`
- The mapped workflow outcome is trigger-aware:
- `hard-fail`: `over-budget` fails the run.
- `soft-warn`: `warning` or `over-budget` marks the run as non-blocking warning.
- `trend-only`: any budget drift is informational only.
## 4. ArtifactPublicationContract
Represents the per-lane artifact staging and upload rules.
| Field | Type | Description |
|---|---|---|
| `contractId` | string | Stable artifact contract id per lane. |
| `laneId` | string | Lane whose artifacts are being exported. |
| `sourceDirectory` | string | Local artifact root, expected to be `storage/logs/test-lanes`. |
| `sourcePatterns` | list<string> | Local file patterns such as `fast-feedback-latest.summary.md`. |
| `requiredFiles` | list<string> | Files that must be exported for a successful lane publication. |
| `optionalFiles` | list<string> | Files exported only when the lane supports them, such as `profile.txt`. |
| `stagingDirectory` | string | Per-run upload directory created by the staging helper. |
| `stagedNamePattern` | string | Deterministic upload naming pattern per lane and run. |
| `uploadGroupName` | string | Artifact bundle name shown in CI. |
| `retentionClass` | enum | `pr-short`, `mainline-medium`, or `scheduled-medium`. |
**Validation rules**
- `summary.md`, `budget.json`, `report.json`, and `junit.xml` are required for all governed lanes.
- `profile.txt` is optional and only expected for `profiling` unless future policy widens it.
- Upload bundles must remain lane-specific and reproducibly named.
## 5. FailureClassification
Represents the one primary reason a governed CI run did not succeed cleanly.
| Field | Type | Description |
|---|---|---|
| `failureClassId` | enum | `test-failure`, `wrapper-failure`, `budget-breach`, `artifact-publication-failure`, or `infrastructure-failure`. |
| `sourceStep` | string | Workflow step or repo helper that originated the classification. |
| `blockingOn` | list<string> | Trigger classes where this failure class is blocking. |
| `summaryLabel` | string | Human-readable label exposed in the CI summary. |
| `remediationHint` | string | Primary next action for contributors or maintainers. |
**Validation rules**
- Every non-success or warning run records exactly one primary failure class.
- Budget warnings cannot obscure wrapper or artifact failures.
- Infrastructure failures remain reserved for checkout, runner, or environment setup issues.
**State transitions**
- `unclassified``classified`
- `classified` then maps to workflow outcome: `blocked`, `warning`, or `informational`.
## 6. CiRunOutcome
Represents the summarized outcome of one workflow-lane execution pair.
| Field | Type | Description |
|---|---|---|
| `runId` | string | Workflow run identifier from CI. |
| `workflowId` | string | Workflow profile that executed. |
| `laneId` | string | Lane that was executed. |
| `testStatus` | enum | `passed` or `failed`. |
| `budgetStatus` | enum | `within-budget`, `warning`, or `over-budget`. |
| `artifactStatus` | enum | `complete` or `incomplete`. |
| `primaryFailureClassId` | string or null | Primary failure class when not cleanly successful. |
| `blockingStatus` | enum | `blocking`, `non-blocking-warning`, or `informational`. |
| `publishedArtifacts` | list<string> | Exported artifact names for the run. |
**State transitions**
- `pending``executing``succeeded`
- `pending``executing``warning`
- `pending``executing``failed`
## 7. ValidationEvidencePack
Represents the acceptance evidence collected for one workflow path during rollout.
| Field | Type | Description |
|---|---|---|
| `evidenceId` | string | Stable validation id per workflow path. |
| `workflowId` | string | Workflow being validated. |
| `sampleTrigger` | string | Example event used during validation. |
| `expectedLaneIds` | list<string> | Lanes expected to run for that workflow. |
| `expectedArtifacts` | list<string> | Artifacts that must be present. |
| `expectedBudgetMode` | string | Expected budget enforcement mode during validation. |
| `expectedFailureClasses` | list<string> | Failure classes that must remain distinguishable. |
| `verificationMethod` | enum | `local-guard`, `manual-ci-run`, or `scheduled-ci-run`. |
| `recordedAt` | datetime string | When the validation evidence was captured. |
## Relationships
- `WorkflowProfile` has many `LaneBinding` records.
- `LaneBinding` has one `BudgetEnforcementProfile` and one `ArtifactPublicationContract`.
- `CiRunOutcome` references one `WorkflowProfile`, one `LaneBinding`, and zero or one `FailureClassification`.
- `ValidationEvidencePack` references one `WorkflowProfile` and the expected `LaneBinding` set.

View File

@ -0,0 +1,188 @@
# Implementation Plan: CI Test Matrix & Runtime Budget Enforcement
**Branch**: `210-ci-matrix-budget-enforcement` | **Date**: 2026-04-17 | **Spec**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/210-ci-matrix-budget-enforcement/spec.md`
**Input**: Feature specification from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/210-ci-matrix-budget-enforcement/spec.md`
## Summary
Operationalize Spec 210 by wiring Gitea Actions workflows under `.gitea/workflows/` to the existing repo-root lane wrappers, treating `dev` as the protected confidence branch, keeping pull request validation intentionally narrow because Gitea validates pull request heads rather than merge-preview refs, extending the existing `TestLaneManifest`/`TestLaneBudget`/`TestLaneReport` seams with trigger-aware budget and failure semantics, and standardizing CI artifact staging and upload without duplicating lane-selection logic in workflow YAML.
## Technical Context
**Language/Version**: PHP 8.4.15 for repo-truth test governance, Bash for repo-root wrappers, and GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/`
**Primary Dependencies**: Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams
**Storage**: SQLite `:memory:` for default lane execution, filesystem artifacts under the app-root contract path `storage/logs/test-lanes`, checked-in workflow YAML under `.gitea/workflows/`, and no new product database persistence
**Testing**: Existing Pest guard suites for lane contracts, focused CI-governance guard tests for workflow/wrapper/artifact policy, and representative live Gitea runs for pull request, `dev` push, scheduled, and manual workflows
**Target Platform**: TenantAtlas monorepo on Gitea Actions with `act_runner`, Docker-isolated jobs, repo-root lane wrappers, and `dev` as the integration branch for broader confidence validation
**Project Type**: Monorepo with a Laravel platform app and separate Astro website; this feature is scoped to platform test governance and repository CI infrastructure
**Performance Goals**: Keep pull request Fast Feedback anchored to the current `200s` lane budget, mainline Confidence to `450s`, Browser to `150s`, and Heavy Governance to the normalized threshold emitted by `TestLaneManifest`; keep workflow overhead limited to artifact staging and upload rather than additional duplicate lane executions
**Constraints**: Repo truth first; no inline test-selection logic in workflows; no new product routes, panels, assets, or dependencies; avoid Gitea-ignored GitHub workflow features such as `concurrency`, `continue-on-error`, `timeout-minutes`, complex multi-label `runs-on`, problem matchers, and annotation-only failure handling; keep PR workflows compatible with Gitea's `refs/pull/:number/head` behavior; prefer explicit workflow files over dynamic job matrices or heavy conditional logic
**Scale/Scope**: Six existing lane entry points (`fast-feedback`, `confidence`, `heavy-governance`, `browser`, `profiling`, `junit`), five artifact modes per lane at most, no existing `.gitea/workflows/` directory, and an established local governance contract already documented in `README.md` and guarded in `apps/platform/tests/Feature/Guards`
### Filament v5 Implementation Notes
- **Livewire v4.0+ compliance**: Preserved. This feature governs CI around existing Filament and Livewire tests and does not alter the runtime Filament stack.
- **Provider registration location**: Unchanged. Existing panel providers remain registered in `bootstrap/providers.php`.
- **Global search rule**: No globally searchable resources are added or changed.
- **Destructive actions**: No runtime destructive actions are introduced. Any affected tests continue to validate existing confirmation and authorization behavior only.
- **Asset strategy**: No panel or shared assets are added. Existing `filament:assets` deployment behavior remains unchanged.
- **Testing plan**: Add or update Pest guards for workflow-to-lane mapping, wrapper-only execution, artifact staging contract, failure classification behavior, and trigger-aware budget semantics, plus representative live Gitea validation for each workflow path.
## Test Governance Impact
- **Affected validation lanes**: `fast-feedback` for blocking pull request validation, `confidence` for `dev` push validation, `heavy-governance` for separate manual and scheduled heavy validation, and `browser` for separate manual and scheduled browser validation. `profiling` and `junit` remain support-only lanes and are not widened by this feature.
- **Fixture/helper cost risk**: Low and bounded to new CI-governance guard files, support-class policy helpers, and `scripts/platform-test-artifacts`. The implementation must not add shared product fixtures, widen default guard setup, or accidentally promote CI-governance tests into Heavy Governance or Browser lanes.
- **Heavy/browser impact**: No new heavy families or browser scenarios are created. The work only makes the existing heavy/browser lanes explicit in CI triggers, artifact bundles, and reviewer guidance.
- **Runtime drift follow-up**: Record Fast Feedback CI variance tolerance, any material runtime drift or recalibration, and the required validation evidence set in the active spec or PR.
- **Required validation evidence set**: one `pull_request` Fast Feedback run, one `push` to `dev` Confidence run, one manual Heavy Governance run, one scheduled Heavy Governance run, one manual Browser run, and one scheduled Browser run, each with trigger, lane, artifact bundle, budget outcome, and primary failure classification; the Fast Feedback record must reference the chosen CI variance tolerance, and any material runtime recalibration must be documented in the active spec or PR and may be linked from the affected evidence.
## Frozen Trigger Matrix
| Trigger class | Workflow profile | Lane binding | Budget mode | Rollout note |
|---------------|------------------|--------------|-------------|--------------|
| `pull-request` | `pr-fast-feedback` | `fast-feedback` | `hard-fail` after documented Fast Feedback CI variance allowance | Always enabled |
| `mainline-push` on `dev` | `main-confidence` | `confidence` | `soft-warn` for budget, blocking for test and artifact failures | Always enabled |
| `manual` heavy | `heavy-governance-manual` | `heavy-governance` | `soft-warn` or `trend-only` until stability improves | Required before enabling schedule |
| `scheduled` heavy | `heavy-governance-scheduled` | `heavy-governance` | `soft-warn` or `trend-only` until stability improves | Disabled until first successful manual run |
| `manual` browser | `browser-manual` | `browser` | `trend-only` or `soft-warn` until stability improves | Required before enabling schedule |
| `scheduled` browser | `browser-scheduled` | `browser` | `trend-only` or `soft-warn` until stability improves | Disabled until first successful manual run |
## No-New-Fixture-Cost Rule
- CI-governance helpers and tests for this feature must remain cheap enough for the default non-browser lanes.
- No shared product fixture expansion, no broader default seeding, and no accidental Heavy Governance or Browser promotion are permitted as part of this rollout.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: PASS. No Inventory, backup, or snapshot truth is changed.
- Read/write separation: PASS. This is repository-only CI and test-governance work and introduces no end-user mutations.
- Graph contract path: PASS. No Graph calls, contract registry changes, or provider runtime integrations are added.
- Deterministic capabilities: PASS. No capability resolver or authorization registry changes.
- RBAC-UX, workspace isolation, tenant isolation: PASS. No runtime route, policy, tenant, or workspace access behavior is changed.
- Run observability and Ops-UX: PASS. CI artifacts remain filesystem outputs and do not introduce `OperationRun` or operator notification behavior.
- Data minimization: PASS. Lane reports, budget summaries, and CI evidence remain repo-local and must not contain secrets or tenant payloads.
- Proportionality and bloat control: PASS WITH LIMITS. The only new semantic layer is a narrow repo-level trigger policy, artifact contract, and failure classification model that extends the existing test-governance seams instead of creating a parallel CI framework.
- TEST-TRUTH-001: PASS WITH WORK. The implementation must keep real test failures and workflow misconfiguration legible instead of downgrading them into generic warning noise.
- Filament/UI constitutions: PASS / NOT APPLICABLE. No operator-facing runtime UI, action surfaces, badges, or panel IA are changed.
**Phase 0 Gate Result**: PASS
- The feature stays bounded to repository CI wiring, test-governance policy, artifacts, and validation evidence.
- No new runtime persistence, product routes, Graph seams, or user-facing surfaces are introduced.
- The plan extends existing lane and artifact seams rather than inventing a second governance system.
## Project Structure
### Documentation (this feature)
```text
specs/210-ci-matrix-budget-enforcement/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│ ├── ci-lane-matrix.schema.json
│ └── ci-lane-governance.logical.openapi.yaml
└── tasks.md
```
### Source Code (repository root)
```text
.gitea/
├── workflows/
│ ├── test-pr-fast-feedback.yml
│ ├── test-main-confidence.yml
│ ├── test-heavy-governance.yml
│ └── test-browser.yml
apps/
├── platform/
│ ├── composer.json
│ ├── tests/
│ │ ├── Feature/Guards/
│ │ └── Support/
│ │ ├── TestLaneBudget.php
│ │ ├── TestLaneManifest.php
│ │ └── TestLaneReport.php
│ └── storage/logs/test-lanes/
scripts/
├── platform-test-lane
├── platform-test-report
└── platform-test-artifacts
README.md
```
**Structure Decision**: Keep workflow logic thin and explicit under `.gitea/workflows/`, keep lane selection and budget truth inside the existing `TestLaneManifest`/`TestLaneBudget`/`TestLaneReport` seams, and add only one narrow repo-root helper for CI artifact staging so artifact naming and upload rules do not become duplicated shell logic across multiple workflows.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| None | Not applicable | Not applicable |
## Proportionality Review
- **Current operator problem**: Contributors and reviewers cannot rely on shared CI to preserve lane discipline, budget honesty, or standardized artifacts across pull request and mainline validation.
- **Existing structure is insufficient because**: The repo already has wrapper scripts, budgets, lane manifests, and reports, but none of that is yet wired into checked-in Gitea workflows with explicit trigger, artifact, and failure semantics.
- **Narrowest correct implementation**: Add explicit workflow files, extend the existing lane/report support classes with trigger-aware budget and failure metadata, and stage per-lane artifacts through a narrow helper instead of duplicating rules in YAML.
- **Ownership cost created**: The repo must maintain four workflow files, one CI artifact staging helper, trigger-aware budget policy, and a small set of CI-governance guard tests.
- **Alternative intentionally rejected**: A single dynamic matrix workflow with GitHub-style concurrency and conditionals, or inline lane logic embedded directly inside workflow files, because that would depend on weaker Gitea compatibility and duplicate repository truth.
- **Release truth**: Current-release repository truth required to institutionalize the already implemented test-governance model from Specs 206 through 209.
## Phase 0 — Research (complete)
- Output: [research.md](./research.md)
- Resolved key decisions:
- Use explicit workflow files per trigger class instead of a single dynamic job matrix because Gitea ignores or limits several GitHub workflow primitives and the current need does not justify indirection.
- Treat `dev` as the protected mainline Confidence branch because repo process already uses `dev` as the integration branch and Gitea pull request refs point to PR heads rather than synthetic merge previews.
- Keep `scripts/platform-test-lane` and `scripts/platform-test-report` as the only lane execution entry points and extend the existing support classes instead of creating a second CI-only selection manifest.
- Do not run the separate `junit` support lane in CI by default; publish the JUnit XML already produced by the Confidence lane to avoid duplicate non-browser cost.
- Stage `*-latest.*` files into a per-run export directory before upload so CI artifacts have stable names even though the local contract remains `lane-latest.*`.
- Introduce trigger-aware budget enforcement profiles with a variance allowance and three outcome classes (`hard-fail`, `soft-warn`, `trend-only`) so Fast Feedback can block on mature overruns while heavier lanes remain non-blocking until their budgets stabilize.
- Encode failure classes in repo-produced summary and JSON artifacts rather than relying on Gitea problem matchers or annotations, because Gitea ignores those GitHub-centric UI features.
- Keep `profiling` outside the default PR/Main matrix and reserve it for manual or follow-up trend work, which aligns with Spec 211 as the next maturity step.
## Phase 1 — Design & Contracts (complete)
- Output: [data-model.md](./data-model.md) formalizes workflow profiles, lane bindings, budget-enforcement profiles, artifact publication contracts, failure classifications, and validation evidence packs.
- Output: [contracts/ci-lane-matrix.schema.json](./contracts/ci-lane-matrix.schema.json) defines the checked-in contract for workflow profiles, trigger-to-lane bindings, artifact requirements, and budget behavior.
- Output: [contracts/ci-lane-governance.logical.openapi.yaml](./contracts/ci-lane-governance.logical.openapi.yaml) captures the logical contract for executing a governed lane, staging artifacts, classifying budget outcomes, and summarizing CI results.
- Output: [quickstart.md](./quickstart.md) provides the implementation order, validation commands, and first-live-run checklist for the CI rollout.
### Post-design Constitution Re-check
- PASS: No runtime routes, panels, Graph seams, or authorization planes are introduced.
- PASS: Trigger policy, artifact staging, and failure classification remain repo-local governance constructs justified by current shared-validation needs.
- PASS: The design extends existing lane/report classes and wrappers rather than adding a generic CI framework or new persistence.
- PASS WITH WORK: Fast Feedback hard-fail budget behavior must include a documented tolerance strategy so CI runner noise does not create brittle red builds.
- PASS WITH WORK: Heavy Governance and Browser must remain explicitly separated from the fast path in both trigger configuration and workflow naming so the CI contract stays legible.
## Phase 2 — Implementation Planning
`tasks.md` should cover:
- Creating `.gitea/workflows/test-pr-fast-feedback.yml` for `pull_request` events (`opened`, `reopened`, `synchronize`) and wiring it only to the Fast Feedback lane.
- Creating `.gitea/workflows/test-main-confidence.yml` for pushes to `dev` and wiring it to the Confidence lane while publishing summary, report, budget, and JUnit artifacts from that same lane run.
- Creating `.gitea/workflows/test-heavy-governance.yml` and `.gitea/workflows/test-browser.yml` for `workflow_dispatch`, with scheduled execution enabled only after the first successful manual validation so heavy and browser cost classes stay separately visible and independently re-runnable.
- Extending `TestLaneManifest` with trigger-aware CI policy metadata so workflows consume lane truth instead of carrying duplicate branch, artifact, or budget rules inline.
- Extending `TestLaneBudget` and `TestLaneReport` so they emit trigger-aware budget outcome classes, single primary failure classes, and CI summary metadata that can be surfaced without GitHub-only annotations.
- Adding the narrow repo-root helper `scripts/platform-test-artifacts` to stage and rename per-lane artifacts from `apps/platform/storage/logs/test-lanes` into a deterministic upload directory.
- Defining the initial budget-enforcement policy: PR Fast Feedback blocking on test, wrapper, manifest, and artifact failures plus mature budget overruns; Main Confidence blocking on test and artifact failures while budget remains warning-first; Heavy Governance and Browser warning-only or trend-only until stability improves; and documenting the Fast Feedback CI variance tolerance explicitly.
- Adding or updating guard tests that verify workflow file presence, wrapper-only lane invocation, wrong-lane and unresolved-entry-point classification, `dev` push mapping, heavy/browser separation, artifact staging completeness, failure-class legibility, and no accidental heavy/browser promotion of CI-governance coverage.
- Updating `README.md` with a concise contributor guide for local reproduction, trigger expectations, blocking semantics, and artifact locations.
- Validating each workflow path with the required evidence set of representative pull request, `dev` push, manual, and scheduled runs, and archiving evidence that documented triggers, expected lanes, published artifacts, and failure semantics all match the contract.
### Contract Implementation Note
- The JSON schema is schema-first and repository-tooling-oriented. It defines what the checked-in CI lane matrix must express even if the first implementation stores most policy in PHP arrays and workflow YAML.
- The OpenAPI file is logical rather than transport-prescriptive. It documents how workflows, wrappers, and reporting helpers interact, not a public HTTP API.
- The design intentionally avoids new database persistence or a separate CI service. Artifacts remain filesystem-based and are uploaded per job after staging.
### Deployment Sequencing Note
- No database migration is planned.
- No asset publish step changes.
- Rollout order should be: extend manifest/report policy fields, add artifact staging helper, land PR Fast Feedback workflow, land `dev` Confidence workflow, land manual Heavy Governance and Browser workflows, enable their schedules after the first successful manual validation, then capture one scheduled Heavy Governance run and one scheduled Browser run as part of rollout evidence.

View File

@ -0,0 +1,108 @@
# Quickstart: CI Test Matrix & Runtime Budget Enforcement
## Preconditions
- Repository Actions are enabled in Gitea for this repository.
- At least one trusted `act_runner` is registered with a single stable runner label.
- The runner can reach the Gitea instance and the action source used by the workflows.
- `dev` remains the integration branch for broader shared validation.
- The existing local lane wrappers still pass before CI wiring begins.
## Required Validation Evidence
- One representative `pull_request` Fast Feedback run.
- One representative `push` to `dev` Confidence run.
- One manual Heavy Governance workflow run.
- One scheduled Heavy Governance workflow run after schedules are enabled.
- One manual Browser workflow run.
- One scheduled Browser workflow run after schedules are enabled.
- Each evidence record must capture the trigger, executed lane, artifact bundle names, budget outcome class, and primary failure class or explicit clean-success result. The Fast Feedback evidence must reference the chosen CI variance tolerance, and any material runtime recalibration must be recorded in `specs/210-ci-matrix-budget-enforcement/spec.md` or the active PR and may be linked from the affected evidence records.
## Frozen Trigger Matrix
| Trigger | Workflow | Lane | Expected artifact bundle |
|---|---|---|---|
| `pull_request` (`opened`, `reopened`, `synchronize`) | `test-pr-fast-feedback.yml` | `fast-feedback` | summary, report, budget, JUnit |
| `push` to `dev` | `test-main-confidence.yml` | `confidence` | summary, report, budget, JUnit |
| `workflow_dispatch` heavy | `test-heavy-governance.yml` | `heavy-governance` | summary, report, budget, JUnit |
| scheduled heavy | `test-heavy-governance.yml` | `heavy-governance` | summary, report, budget, JUnit |
| `workflow_dispatch` browser | `test-browser.yml` | `browser` | summary, report, budget, JUnit |
| scheduled browser | `test-browser.yml` | `browser` | summary, report, budget, JUnit |
## No-New-Fixture-Cost Rule
- Keep CI-governance additions limited to manifest or budget metadata, lightweight guard tests, workflow files, and the artifact staging helper.
- Do not add shared product fixtures, broaden default guard setup, or move CI-governance checks into Heavy Governance or Browser by default.
- Treat the Fast Feedback CI variance allowance as `15s`; a pull request lane run between `200s` and `215s` is warning-only, while a run above `215s` becomes a blocking budget breach.
## Artifact Locations
- Lane-local machine-readable outputs remain under `apps/platform/storage/logs/test-lanes`.
- CI upload staging directories are `.gitea-artifacts/pr-fast-feedback`, `.gitea-artifacts/main-confidence`, `.gitea-artifacts/heavy-governance`, and `.gitea-artifacts/browser`.
- Scheduled Heavy Governance is gated by `TENANTATLAS_ENABLE_HEAVY_GOVERNANCE_SCHEDULE=1` and scheduled Browser is gated by `TENANTATLAS_ENABLE_BROWSER_SCHEDULE=1`.
## Implementation Order
1. Extend repo truth first.
- Add trigger-aware CI policy fields to `TestLaneManifest`.
- Extend `TestLaneBudget` and `TestLaneReport` with failure-class and CI-summary metadata.
- Add `scripts/platform-test-artifacts` for deterministic export and upload staging.
2. Land the blocking pull request path.
- Add `.gitea/workflows/test-pr-fast-feedback.yml`.
- Use only `scripts/platform-test-lane fast-feedback`.
- Stage and upload the Fast Feedback artifacts.
3. Land the broader mainline path.
- Add `.gitea/workflows/test-main-confidence.yml`.
- Trigger it on `push` to `dev`.
- Publish the Confidence lane JUnit XML, summary, report, and budget outputs from the same run.
4. Land the expensive lanes as separate workflows.
- Add `.gitea/workflows/test-heavy-governance.yml` for `workflow_dispatch`, with the schedule enabled only after the first successful manual validation.
- Add `.gitea/workflows/test-browser.yml` for `workflow_dispatch`, with the schedule enabled only after the first successful manual validation.
- Keep their failure semantics non-blocking at first unless stability evidence justifies more.
5. Add guards and documentation.
- Add CI-governance guard tests, including wrong-lane and unresolved-entry-point failure classification.
- Update `README.md` with trigger policy, local reproduction, artifact locations, and blocking semantics.
## Local Validation Commands
Run the existing wrappers before pushing workflow changes:
```bash
./scripts/platform-test-lane fast-feedback
./scripts/platform-test-lane confidence
./scripts/platform-test-lane heavy-governance
./scripts/platform-test-lane browser
./scripts/platform-test-report fast-feedback
./scripts/platform-test-report confidence
```
Run the focused guard suites after implementation changes:
```bash
cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php tests/Feature/Guards/CiConfidenceWorkflowContractTest.php tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php tests/Feature/Guards/CiLaneFailureClassificationContractTest.php tests/Feature/Guards/FastFeedbackLaneContractTest.php tests/Feature/Guards/ConfidenceLaneContractTest.php tests/Feature/Guards/HeavyGovernanceLaneContractTest.php tests/Feature/Guards/BrowserLaneIsolationTest.php tests/Feature/Guards/FixtureLaneImpactBudgetTest.php tests/Feature/Guards/TestLaneManifestTest.php tests/Feature/Guards/TestLaneArtifactsContractTest.php tests/Feature/Guards/TestLaneCommandContractTest.php
```
## First Live Gitea Validation
1. Push the workflow files and helper changes on a feature branch.
2. Open or update a pull request and confirm only the Fast Feedback workflow triggers.
3. Verify the uploaded artifact bundle contains lane-specific summary, report, budget, and JUnit files.
4. Push or merge to `dev` and confirm the Confidence workflow triggers and publishes the broader artifact set.
5. Manually dispatch Heavy Governance and Browser workflows and confirm they remain separate.
6. Review the manual run summaries and guard results to confirm failure classification stays legible and the CI-governance changes did not widen fast-path fixture cost or lane membership unexpectedly.
7. Enable schedules only after the manual runs succeed and their artifact bundles are legible.
8. Capture one scheduled Heavy Governance run and one scheduled Browser run, then record trigger, executed lane, bundle names, budget outcome, and the primary failure class or explicit clean-success result in this quickstart, and record the Fast Feedback variance tolerance or any material recalibration note in `specs/210-ci-matrix-budget-enforcement/spec.md` or the active PR.
## Review Checklist
- Pull request workflow uses the repo wrapper and no inline test paths.
- `dev` push workflow publishes Confidence artifacts from one lane run.
- Heavy Governance and Browser have separate workflow files and separate upload bundles.
- Artifact names are deterministic per run and per lane.
- Failure summaries distinguish test, budget, artifact, wrapper, manifest, and infrastructure problems.
- README explains what blocks a pull request and what only warns.
- CI-governance changes do not widen default fixtures or promote guard coverage into Heavy Governance or Browser unexpectedly.

View File

@ -0,0 +1,65 @@
# Research: CI Test Matrix & Runtime Budget Enforcement
## Decision 1: Use explicit workflow files per trigger class
- **Decision**: Implement four explicit Gitea workflow files under `.gitea/workflows/`: one for pull request Fast Feedback, one for `dev` push Confidence, one for Heavy Governance, and one for Browser.
- **Rationale**: The repo currently has no checked-in CI workflows, and Gitea Actions ignores or limits several GitHub workflow features that would otherwise encourage a single dynamic matrix file (`concurrency`, `continue-on-error`, `timeout-minutes`, complex multi-label `runs-on`, problem matchers, and annotation-heavy UX). Separate workflows keep trigger policy, lane ownership, and failure semantics legible.
- **Alternatives considered**:
- A single GitHub-style matrix workflow with conditional jobs: rejected because it depends on more workflow indirection and weaker Gitea compatibility.
- A reusable workflow plus caller stubs: rejected because the repo does not yet need that abstraction and the initial rollout benefits from explicit files.
## Decision 2: Treat `dev` as the mainline confidence branch
- **Decision**: Use `push` to `dev` as the mainline Confidence trigger and keep pull request validation limited to Fast Feedback by default.
- **Rationale**: Repository process already uses `dev` as the integration branch. Gitea pull request refs point to `refs/pull/:number/head`, not a merge-preview ref, so the pull request path should stay fast and deterministic while `dev` push Confidence validates integrated state.
- **Alternatives considered**:
- Run Confidence on every pull request: rejected because it would widen the highest-frequency path and duplicate the broader validation already expected on integration.
- Run Confidence on every branch push: rejected because it would over-trigger broad validation on work-in-progress branches and dilute the meaning of mainline evidence.
## Decision 3: Keep repo wrappers and `TestLaneManifest` as the execution source of truth
- **Decision**: Make `scripts/platform-test-lane` and `scripts/platform-test-report` the only CI execution entry points and extend `TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` for CI policy metadata.
- **Rationale**: The repo already owns lane membership, command refs, budgets, report building, and artifact paths in checked-in support code. CI should consume that truth rather than rebuilding lane selection or budget logic in YAML.
- **Alternatives considered**:
- Inline `sail composer run ...` commands in workflow files: rejected because it would duplicate repo truth and invite lane drift.
- A second CI-only manifest file: rejected because it would create two governance sources for the same lane model.
## Decision 4: Publish JUnit from the existing lane run, not a second CI rerun
- **Decision**: Use the JUnit XML already emitted by the governing lane run, especially for Confidence, instead of adding a second dedicated `junit` CI execution by default.
- **Rationale**: Fast Feedback, Confidence, Browser, and Heavy Governance already write JUnit XML alongside summary and budget artifacts. Re-running the same non-browser scope only for machine-readable output would inflate CI cost without increasing trust.
- **Alternatives considered**:
- Always run the dedicated `junit` support lane in CI: rejected because it duplicates Confidence-shaped scope and adds avoidable runtime.
- Publish terminal output only: rejected because Spec 210 requires machine-readable artifacts as part of the CI contract.
## Decision 5: Stage per-lane artifacts into a CI export directory before upload
- **Decision**: Add one narrow repo-root helper, `scripts/platform-test-artifacts`, to copy lane-local `*-latest.*` outputs into a deterministic per-run export directory with stable upload names.
- **Rationale**: The local artifact contract intentionally uses `lane-latest.*` files under `apps/platform/storage/logs/test-lanes`. CI upload needs a per-run directory and naming scheme that remains comparable and lane-specific without changing the local contract or duplicating file-copy logic in every workflow.
- **Alternatives considered**:
- Upload the raw `*-latest.*` files directly: rejected because upload naming would stay ambiguous and harder to compare across runs.
- Persist artifacts into a database table: rejected because the feature is repository CI governance, not product persistence.
## Decision 6: Make budget enforcement trigger-aware and tolerance-aware
- **Decision**: Add trigger-aware budget enforcement profiles that derive from the existing lane budgets or heavy-governance contract and apply a documented CI variance allowance before classifying outcomes as `hard-fail`, `soft-warn`, or `trend-only`.
- **Rationale**: Existing budgets are all `warn` today and were measured in local or pre-CI conditions. Fast Feedback can become blocking only if CI runner noise is accounted for explicitly; Confidence, Heavy Governance, and Browser need softer rollout semantics until their CI baselines are proven stable.
- **Alternatives considered**:
- Hard-fail every budget from day one: rejected because it would overreact to runner variance and immature heavy-lane baselines.
- Keep all budget results as warnings forever: rejected because it would fail to institutionalize the new governance model.
## Decision 7: Encode failure classes in repo-produced artifacts, not UI annotations
- **Decision**: Emit a single primary failure classification per non-success run through repo-produced summary and JSON artifacts, distinguishing test failure, wrapper or manifest failure, budget breach, artifact publication failure, and infrastructure failure.
- **Rationale**: Gitea ignores GitHub problem matchers and annotation-centric UX, so the failure contract must be legible through uploaded artifacts and ordinary job logs. This also keeps the classification logic versioned in the repo.
- **Alternatives considered**:
- Depend on GitHub-style annotations: rejected because Gitea ignores them.
- Treat every non-success as a generic failed run: rejected because Spec 210 requires failure classes to remain distinguishable.
## Decision 8: Keep profiling out of the initial CI matrix and schedule heavy lanes separately
- **Decision**: Leave `profiling` as a manual or follow-up support lane, while Heavy Governance and Browser get their own manual plus scheduled workflows rather than sharing PR or mainline triggers.
- **Rationale**: Profiling exists to explain drift, not to gate ordinary contributor flow. Heavy Governance and Browser are important but intentionally expensive, so they should be visible and reproducible in CI without silently widening the fast path.
- **Alternatives considered**:
- Add profiling to the first CI rollout: rejected because it adds extra cost without being part of the core acceptance path for Spec 210.
- Fold Heavy Governance or Browser into PR or `dev` workflows: rejected because it would reintroduce the very cost drift that Specs 206 through 209 separated.

View File

@ -0,0 +1,236 @@
# Feature Specification: CI Test Matrix & Runtime Budget Enforcement
**Feature Branch**: `210-ci-matrix-budget-enforcement`
**Created**: 2026-04-17
**Status**: Draft
**Input**: User description: "Spec 210 — CI Test Matrix & Runtime Budget Enforcement"
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
- **Problem**: TenantPilot's test-lane governance is now credible locally, but shared repository validation still does not enforce the agreed lane paths, runtime budgets, or artifact contract.
- **Today's failure**: Pull requests can silently widen the fast path, budget drift can accumulate without shared visibility, expensive lanes can bleed into the wrong triggers, and reviewers cannot rely on standardized CI evidence.
- **User-visible improvement**: Contributors and reviewers get predictable fast, confidence, heavy, and browser validation paths with visible budget outcomes, standardized artifacts, and clear blocking versus non-blocking semantics.
- **Smallest enterprise-capable version**: Wire repository CI to the existing checked-in lane entry points, classify each lane's budget outcome as blocking, warning, or informational per trigger, standardize per-lane artifacts, and document how contributors reproduce and interpret the governed paths.
- **Explicit non-goals**: No new lane taxonomy, no new fixture-slimming or hotspot-optimization program, no broad build or deploy redesign, no new browser strategy, and no historical trend platform beyond per-run visibility.
- **Permanent complexity imported**: Trigger-to-lane policy, budget enforcement classes, standardized artifact naming and retention rules, failure classification vocabulary, validation evidence, and concise contributor guidance.
- **Why now**: Specs 206 through 209 created the necessary lane wrappers, fixture cost reductions, heavy-lane separation, and budget honesty; without CI enforcement those gains can drift during ordinary team and PR flow.
- **Why not local**: Local wrappers and scripts cannot protect shared pull request behavior, enforce consistent blocking semantics, or publish comparable artifacts for reviewers and maintainers.
- **Approval class**: Cleanup
- **Red flags triggered**: New CI enforcement vocabulary and harder policy semantics. Defense: the scope stays repository-level, reuses existing lane truth, and avoids inventing a parallel execution model or new product runtime structures.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: workspace
- **Primary Routes**: No end-user HTTP routes change. The affected surfaces are repository-owned CI validation paths, checked-in lane entry points, run summaries, artifact outputs, and contributor guidance.
- **Data Ownership**: Workspace-owned CI workflow definitions, lane policy, artifact naming conventions, budget classification results, validation evidence, and contributor documentation. No tenant-owned records or product runtime tables are introduced.
- **RBAC**: No end-user authorization behavior changes. The affected actors are contributors, reviewers, maintainers, and CI runners operating against the shared test-governance contract.
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: yes, but only repository-owned CI artifacts and validation outputs; no new product database persistence is introduced
- **New abstraction?**: yes, but limited to a repository-level trigger-to-lane enforcement model, artifact contract, and budget outcome policy
- **New enum/state/reason family?**: yes, but only repository-level failure and budget classification values used to keep CI outcomes legible
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: Contributors and reviewers cannot rely on CI to preserve lane discipline, budget honesty, or standardized evidence across pull request and mainline work.
- **Existing structure is insufficient because**: Local wrappers, budgets, and reports prove the governance model conceptually, but they do not stop shared workflows from drifting or guarantee that reviewers see the same artifacts and enforcement semantics.
- **Narrowest correct implementation**: Reuse the existing lane wrappers and budgets, map them to explicit CI triggers, standardize artifacts and failure classes, and document how to reproduce each governed path locally.
- **Ownership cost**: The team must maintain trigger policy, artifact naming, budget classes, validation coverage, and contributor guidance as lane budgets and runner behavior evolve.
- **Alternative intentionally rejected**: Inline CI commands or a one-size-fits-all full-suite gate, because both duplicate repository truth and would either hide drift or overburden the fast path.
- **Release truth**: Current-release repository truth required to operationalize the already approved local governance work from Specs 206 through 209.
## Problem Statement
TenantPilot has already reshaped the test suite structurally:
- Lane governance and runtime budgets now exist.
- Shared fixture cost has been reduced.
- Heavy Filament or Livewire families have been separated from lighter paths.
- Heavy-governance cost has been made honest and explicitly visible.
That progress is still vulnerable to drift until repository CI becomes the enforcement surface.
The open risks are now operational rather than conceptual:
- Shared pull request validation can still grow beyond the intended fast path.
- Runtime budgets remain advisory until CI classifies and reacts to overruns.
- JUnit-style results, lane reports, and budget evidence remain too easy to treat as local debugging aids instead of shared run outputs.
- Heavy Governance and Browser have a different cost class from Fast Feedback and Confidence, but they are not yet guaranteed to stay on separate trigger cadences.
- Without explicit failure semantics, the team either over-blocks the normal flow or under-enforces the very governance it just created.
Without this feature, the repository has a better-organized suite but not yet an institutionalized validation contract.
## Dependencies
- Depends on Spec 206 - Test Suite Governance & Performance Foundation for lane vocabulary, budgets, and checked-in entry points.
- Depends on Spec 207 - Shared Test Fixture Slimming for the reduced default per-test cost that makes lane budgets credible.
- Depends on Spec 208 - Heavy Suite Segmentation for the honest separation of heavy Filament or Livewire families.
- Depends on Spec 209 - Heavy Governance Lane Cost Reduction for a more stable heavy-lane budget basis before CI enforcement hardens.
- Recommended after stable local lane wrappers, credible initial budgets, and a clean Heavy Governance classification.
- Blocks durable team-wide enforcement of the new test-governance model in everyday pull request and mainline flow.
- Does not block isolated local development while contributors still follow the existing lane rules manually.
## Goals
- Establish the existing lane wrappers as the required CI execution paths.
- Make runtime budgets machine-checked and visible in shared validation.
- Separate pull request, mainline, scheduled, and manual validation intentionally.
- Standardize machine-readable test results, lane reports, and budget evidence as part of the CI contract.
- Surface lane drift and budget erosion early enough to correct.
- Distinguish blocking from informative signals clearly.
- Make the repository's test governance durable for the team rather than dependent on local discipline.
## Non-Goals
- Creating another fixture-slimming effort.
- Re-segmenting the lane model created in earlier specs.
- Fixing every individual performance hotspot inside this spec.
- Redesigning the broader build, deploy, or environment pipeline.
- Replacing the existing browser strategy beyond its CI placement and enforcement level.
- Introducing a long-horizon historical trend platform; this spec is about per-run enforcement and visibility.
## Assumptions
- The lane wrappers and budget definitions established by Specs 206 through 209 are mature enough to serve as CI entry points with only targeted hardening.
- Repository CI can retain per-lane artifacts and expose them to contributors or reviewers for governed runs.
- Fast Feedback and Confidence can become stricter earlier than Heavy Governance or Browser if the latter still need softer enforcement while their budgets stabilize.
- Validation can use representative CI runs to prove the matrix, without requiring multi-week trend infrastructure in this feature.
## Test Governance Impact *(mandatory — TEST-GOV-001)*
- **Affected validation lanes**: `fast-feedback` for blocking pull request validation, `confidence` for `dev` push validation, `heavy-governance` for separate manual and scheduled heavy validation, and `browser` for separate manual and scheduled browser validation. `profiling` and `junit` remain support-only lanes outside the default CI trigger matrix.
- **Fixture/helper cost risk**: Low and bounded. This feature adds CI workflow files, CI-governance guards, and an artifact staging helper only. It MUST NOT introduce new shared product fixtures, widen default guard setup, or accidentally promote CI-governance coverage into Heavy Governance or Browser lane membership.
- **Heavy/browser impact**: No new browser scenarios or heavy-governance families are introduced. The feature only operationalizes the existing Heavy Governance and Browser lanes as explicit CI trigger classes and evidence bundles.
- **Budget/baseline follow-up**: Fast Feedback hard-fail budget enforcement requires a documented CI variance tolerance before rollout is complete. Any material runtime drift or recalibration discovered during rollout MUST be recorded in this spec or the implementation PR.
## Required Validation Evidence Set
- One representative `pull_request` Fast Feedback run.
- One representative `push` to `dev` Confidence run.
- One manual `heavy-governance` workflow run.
- One scheduled `heavy-governance` workflow run after schedules are enabled.
- One manual `browser` workflow run.
- One scheduled `browser` workflow run after schedules are enabled.
- Each evidence record MUST identify the trigger, executed lane, published artifact bundle, budget outcome class, and primary failure class or explicit clean-success result. The Fast Feedback evidence record MUST reference the chosen CI variance tolerance, and any material runtime recalibration discovered during rollout MUST be recorded in this spec or the implementation PR and may be linked from the affected evidence records.
## Artifact Publication Contract
- Pull request runs stage upload bundles in `.gitea-artifacts/pr-fast-feedback`.
- `dev` confidence runs stage upload bundles in `.gitea-artifacts/main-confidence`.
- Heavy Governance runs stage upload bundles in `.gitea-artifacts/heavy-governance`.
- Browser runs stage upload bundles in `.gitea-artifacts/browser`.
- Every governed bundle MUST contain `summary.md`, `budget.json`, `report.json`, and `junit.xml` for the lane executed by that workflow.
## Frozen Trigger Matrix
| Trigger | Workflow profile | Executed lane | Blocking semantics | Schedule state |
|---|---|---|---|---|
| `pull_request` (`opened`, `reopened`, `synchronize`) | `pr-fast-feedback` | `fast-feedback` | Blocking for test, wrapper or manifest, artifact, and mature hard-fail budget failures | N/A |
| `push` to `dev` | `main-confidence` | `confidence` | Blocking for test, wrapper or manifest, and artifact failures; budget remains visible as warning-first | N/A |
| `workflow_dispatch` heavy run | `heavy-governance-manual` | `heavy-governance` | Warning-first / trend-oriented while baselines stabilize | Enabled at rollout |
| Scheduled heavy run | `heavy-governance-scheduled` | `heavy-governance` | Warning-first / trend-oriented while baselines stabilize | Enable only after one successful manual validation |
| `workflow_dispatch` browser run | `browser-manual` | `browser` | Warning-first / trend-oriented while baselines stabilize | Enabled at rollout |
| Scheduled browser run | `browser-scheduled` | `browser` | Informational or warning-first until stability evidence justifies more | Enable only after one successful manual validation |
## No-New-Fixture-Cost Rule
- CI-governance changes in this spec MUST stay inside repo-level workflows, lightweight guard coverage, manifest or budget policy, and artifact staging.
- The feature MUST NOT add shared product fixtures, broaden default setup in existing feature tests, or promote CI-governance coverage into Heavy Governance or Browser lane membership.
- The documented Fast Feedback CI variance allowance is `15s` above the 200 second baseline threshold before the pull request path upgrades a budget overrun from warning to blocking failure.
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Enforce The Fast Pull Request Path (Priority: P1)
As a contributor opening a pull request, I want CI to run only the intended fast validation path and to fail quickly when that path or its hard runtime budget contract is violated.
**Why this priority**: Pull request validation is the highest-frequency shared feedback loop. If it stays ambiguous or slow, the rest of the governance model loses credibility.
**Independent Test**: Run a representative pull request validation against a minimal non-lane-expanding sample change, such as a documentation-only edit or workflow-comment change on a feature branch, and confirm that only the assigned fast lane executes, required artifacts are produced, and blocking test or budget failures stop the run.
**Acceptance Scenarios**:
1. **Given** a contributor opens or updates a pull request, **When** CI starts validation, **Then** it invokes only the assigned fast lane entry point and does not also execute Heavy Governance or Browser lanes.
2. **Given** the fast lane has a test failure or a blocking budget breach, **When** the run completes, **Then** the pull request path is marked failed and the summary identifies the failure class.
3. **Given** the fast lane succeeds, **When** the run completes, **Then** the run exposes the required per-lane artifacts and budget outcome from that same execution.
---
### User Story 2 - Publish Mainline Confidence Evidence (Priority: P1)
As a maintainer protecting shared quality, I want the broader mainline validation path to run the intended confidence checks and publish standardized evidence that reviewers can inspect without reconstructing the run manually.
**Why this priority**: The repository needs a shared confidence path between quick author feedback and the expensive heavy lanes. That path must be broad enough to trust and explicit enough to review.
**Independent Test**: Execute a representative mainline validation run and verify that the assigned confidence lane executes, machine-readable test results and lane reports are published, and the budget result is classified separately from test success or failure.
**Acceptance Scenarios**:
1. **Given** a push reaches the protected mainline path, **When** repository validation runs, **Then** CI executes the assigned broader lane and publishes the required lane artifacts for that path.
2. **Given** the broader lane exceeds a budget with non-blocking semantics, **When** the run completes, **Then** the budget warning remains visible without being confused with a test failure.
3. **Given** the broader lane has a real test failure, **When** a reviewer inspects the run, **Then** the machine-readable results and lane-specific report are available without requiring a local rerun.
---
### User Story 3 - Keep Heavy And Browser Validation Separate (Priority: P2)
As a maintainer or release owner, I want Heavy Governance and Browser validation to remain available in CI as separate cost classes so the normal fast path stays lean while the expensive lanes still stay visible and governable.
**Why this priority**: Expensive lanes remain important, but they should not silently piggyback onto ordinary pull request feedback or become invisible because they are too awkward to run and interpret.
**Independent Test**: Execute representative scheduled or manual Heavy Governance and Browser runs and confirm that each path runs separately, publishes lane-specific artifacts, and shows its declared enforcement semantics.
**Acceptance Scenarios**:
1. **Given** a scheduled or manually requested Heavy Governance run, **When** it executes, **Then** only the assigned heavy lane runs and its budget result is classified according to policy.
2. **Given** a scheduled or manually requested Browser run, **When** it executes, **Then** Browser evidence and artifacts are published separately from the fast and confidence paths.
3. **Given** a contributor checks the repository guidance, **When** they look up expected triggers, **Then** they can tell which triggers run Fast Feedback, Confidence, Heavy Governance, and Browser validation.
### Edge Cases
- What happens when a lane passes tests but fails to publish one or more required artifacts?
- How does the system handle a lane whose budget definition is missing, unreadable, or inconsistent with the declared trigger policy?
- What happens when a run lands close to a budget threshold because of normal runner variability?
- How does the system handle a trigger that points to a lane entry point or manifest that no longer resolves?
- What happens when a contributor manually reruns one expensive lane and CI would otherwise be tempted to execute unrelated lanes implicitly?
## Requirements *(mandatory)*
**Constitution alignment (required):** This feature is repository-only test-governance work. It introduces no Microsoft Graph calls, no product write behavior, no `OperationRun`, and no end-user authorization changes.
**Constitution alignment (PROP-001 / ABSTR-001 / PERSIST-001 / STATE-001 / BLOAT-001):** The feature adds repository-level artifacts, policy abstractions, and failure classes only because local governance alone is insufficient to keep shared validation honest. The Proportionality Review above explains why this is the narrowest correct implementation and why a more local workaround is not enough.
### Functional Requirements
- **FR-001**: The repository MUST define distinct CI validation paths for at least Fast Feedback pull request validation, broader mainline Confidence validation, Heavy Governance validation, and Browser validation.
- **FR-002**: Each governed CI path MUST invoke the repository's checked-in lane entry point for that lane instead of inlining its own test-selection logic.
- **FR-003**: The pull request Fast Feedback path MUST be blocking and MUST exclude Heavy Governance and Browser validation unless an explicitly documented trigger requests them.
- **FR-004**: The mainline Confidence path MUST publish machine-readable test results, a lane report, and a budget evaluation for its assigned lane or lane set.
- **FR-005**: Every governed lane MUST have a documented budget policy that states the budget target, the enforcement class for overruns, and the trigger contexts where that class applies.
- **FR-006**: CI output MUST distinguish at least these failure classes: test failure, wrapper or manifest failure, budget breach or warning, artifact publication failure, and infrastructure or runner failure.
- **FR-007**: Each lane run MUST produce standardized, lane-identifiable artifacts with reproducible naming and storage rules so runs remain comparable over time.
- **FR-008**: The trigger policy MUST document and implement which lane set runs on pull request updates, protected-branch pushes, scheduled runs, and manual runs.
- **FR-009**: The CI contract MUST surface drift signals when a lane exceeds its time budget, produces missing or corrupt artifacts, invokes the wrong lane, or fails to resolve its checked-in entry point.
- **FR-010**: Contributor guidance MUST explain local reproduction, blocking versus non-blocking signals, artifact locations, and when Heavy Governance or Browser runs are expected.
- **FR-011**: Completion of this feature MUST include validation evidence showing that each documented trigger-to-lane pairing executes as declared and produces the expected artifacts and outcome classification.
### Key Entities *(include if feature involves data)*
- **Lane Class**: The governed execution family for a run, including Fast Feedback, Confidence, Heavy Governance, and Browser, with a defined cost class and intended trigger usage.
- **Trigger Policy**: The checked-in mapping from pull request, protected-branch, scheduled, and manual triggers to the lane classes they are allowed or required to execute.
- **Budget Policy**: The per-lane runtime contract that states the target runtime, the enforcement class for overruns, and the contexts where that class is blocking, warning, or informational.
- **Artifact Set**: The machine-readable test results, lane report, budget evaluation, and optional run overview that together form the evidence contract for a governed run.
- **Failure Classification**: The named outcome family used to explain why a governed run did not succeed cleanly, such as test failure, budget problem, artifact failure, or infrastructure failure.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: Within the required validation evidence set for this feature, 100% of pull request runs execute only the intended Fast Feedback lane and do not unintentionally invoke Heavy Governance or Browser validation.
- **SC-002**: Within the required validation evidence set for this feature, 100% of mainline Confidence runs publish the required machine-readable test result, lane report, and budget evaluation for the assigned lane or lane set.
- **SC-003**: Within the required validation evidence set for this feature, 100% of Heavy Governance and Browser runs publish lane-specific artifacts and display the enforcement class declared for that lane.
- **SC-004**: For every governed lane run captured in the required validation evidence set, any non-success or warning outcome is labeled with exactly one named failure class in the run summary.
- **SC-005**: 100% of documented CI triggers represented in the required validation evidence set match their implemented lane mapping, with no undocumented trigger exceptions.
- **SC-006**: Contributors can reproduce each governed lane locally using only documented checked-in entry points, with no undocumented fallback commands required.

View File

@ -0,0 +1,202 @@
# Tasks: CI Test Matrix & Runtime Budget Enforcement
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/210-ci-matrix-budget-enforcement/`
**Prerequisites**: `plan.md` (required), `spec.md` (required), `research.md`, `data-model.md`, `contracts/`, `quickstart.md`
**Tests**: Required. This feature changes repository CI and test-governance behavior, so each user story includes Pest guard coverage and focused validation through Sail and the repo-root lane wrappers.
**Organization**: Tasks are grouped by user story so each story can be implemented and validated independently where possible.
## Phase 1: Setup (Shared Context)
**Purpose**: Freeze shared CI assumptions, lane classification, and rollout evidence expectations before workflow implementation.
- [X] T001 [P] Audit `scripts/platform-test-lane`, `scripts/platform-test-report`, `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Support/TestLaneBudget.php`, and `apps/platform/tests/Support/TestLaneReport.php` as the only valid CI execution and policy seams before writing `.gitea/workflows/*.yml`
- [X] T002 [P] Update `specs/210-ci-matrix-budget-enforcement/spec.md`, `specs/210-ci-matrix-budget-enforcement/plan.md`, and `specs/210-ci-matrix-budget-enforcement/quickstart.md` with the frozen trigger matrix, required validation evidence set, and no-new-fixture-cost rule before workflow implementation
---
## Phase 2: Foundational (Blocking Prerequisites)
**Purpose**: Extend the shared lane manifest, budget, report, and wrapper seams that every workflow path depends on.
**Critical**: No user story work should begin until this phase is complete.
- [X] T003 Extend `apps/platform/tests/Support/TestLaneManifest.php` with workflow profiles, lane bindings, artifact publication contracts, and trigger-to-lane metadata aligned to `specs/210-ci-matrix-budget-enforcement/contracts/ci-lane-matrix.schema.json`
- [X] T004 [P] Extend `apps/platform/tests/Support/TestLaneBudget.php` with trigger-aware enforcement profiles, variance allowance, and blocking-status evaluation for `hard-fail`, `soft-warn`, and `trend-only`
- [X] T005 [P] Extend `apps/platform/tests/Support/TestLaneReport.php` with CI summary metadata, primary failure classification, and artifact publication status aligned to `specs/210-ci-matrix-budget-enforcement/contracts/ci-lane-governance.logical.openapi.yaml`
- [X] T006 [P] Update `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php`, `apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php`, `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php`, and add `apps/platform/tests/Feature/Guards/CiLaneFailureClassificationContractTest.php` to lock the shared CI matrix metadata, artifact publication contract, wrong-lane drift signals, unresolved checked-in entry-point handling, and single-primary-failure classification
- [X] T007 Update `scripts/platform-test-lane`, `scripts/platform-test-report`, and `scripts/platform-test-artifacts` so local and CI executions share the same lane and report export contract
**Checkpoint**: The shared CI-governance seams are ready for story-specific workflow implementation.
---
## Phase 3: User Story 1 - Enforce The Fast Pull Request Path (Priority: P1) 🎯 MVP
**Goal**: Make pull request validation run only the Fast Feedback lane and fail clearly on blocking problems.
**Independent Test**: Open or update a pull request and confirm only the Fast Feedback workflow runs, publishes the required artifacts, and blocks on test, wrapper, artifact, or hard-fail budget problems.
### Tests for User Story 1
- [X] T008 [P] [US1] Add `apps/platform/tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php` and expand `apps/platform/tests/Feature/Guards/FastFeedbackLaneContractTest.php` to assert pull request trigger mapping, fast-only lane execution, and blocking Fast Feedback failure semantics
### Implementation for User Story 1
- [X] T009 [US1] Implement `.gitea/workflows/test-pr-fast-feedback.yml` for `pull_request` events (`opened`, `reopened`, `synchronize`) using `scripts/platform-test-lane fast-feedback`
- [X] T010 [US1] Update `apps/platform/tests/Support/TestLaneManifest.php` and `apps/platform/tests/Support/TestLaneBudget.php` with the `pr-fast-feedback` workflow profile, Fast Feedback artifact bundle requirements, and hard-fail policy for test, wrapper, manifest, artifact, and mature budget failures
- [X] T011 [US1] Wire deterministic Fast Feedback artifact staging and upload in `.gitea/workflows/test-pr-fast-feedback.yml` and `scripts/platform-test-artifacts` so pull request runs publish summary, report, budget, and JUnit bundles
**Checkpoint**: Pull requests now have one explicit blocking fast path that is lane-correct and artifact-complete.
---
## Phase 4: User Story 2 - Publish Mainline Confidence Evidence (Priority: P1)
**Goal**: Make the `dev` branch run the broader Confidence lane and publish machine-readable evidence from that same run.
**Independent Test**: Push to `dev` and confirm the Confidence workflow runs, publishes summary/report/budget/JUnit artifacts, and keeps budget warnings distinct from blocking test or artifact failures.
### Tests for User Story 2
- [X] T012 [P] [US2] Add `apps/platform/tests/Feature/Guards/CiConfidenceWorkflowContractTest.php` and expand `apps/platform/tests/Feature/Guards/ConfidenceLaneContractTest.php` plus `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php` to assert `dev` push mapping, Confidence artifact completeness, and soft budget warning semantics
### Implementation for User Story 2
- [X] T013 [US2] Implement `.gitea/workflows/test-main-confidence.yml` for `push` to `dev` using `scripts/platform-test-lane confidence`
- [X] T014 [US2] Update `apps/platform/tests/Support/TestLaneManifest.php` with the `main-confidence` workflow profile, `dev` branch filter, and Confidence artifact publication contract
- [X] T015 [US2] Update `apps/platform/tests/Support/TestLaneBudget.php` and `apps/platform/tests/Support/TestLaneReport.php` so mainline Confidence distinguishes blocking test/artifact failures from non-blocking budget warnings and publishes the lane's own JUnit XML without a separate `junit` rerun
- [X] T016 [US2] Wire Confidence artifact staging and upload in `.gitea/workflows/test-main-confidence.yml` and `scripts/platform-test-artifacts` so `dev` runs publish summary, report, budget, and JUnit bundles from a single Confidence lane execution
**Checkpoint**: The integration branch now has a broader, reviewable Confidence path with standardized machine-readable evidence.
---
## Phase 5: User Story 3 - Keep Heavy And Browser Validation Separate (Priority: P2)
**Goal**: Run Heavy Governance and Browser as separate scheduled or manual CI lanes with their own semantics and contributor guidance.
**Independent Test**: Trigger Heavy Governance and Browser manually, confirm they run through separate workflows and artifact bundles, and verify the documented trigger matrix explains when each lane is expected.
### Tests for User Story 3
- [X] T017 [P] [US3] Add `apps/platform/tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php` and expand `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php` plus `apps/platform/tests/Feature/Guards/BrowserLaneIsolationTest.php` to assert separate manual or scheduled workflows, non-pull-request isolation, and trigger-specific budget modes
### Implementation for User Story 3
- [X] T018 [P] [US3] Implement `.gitea/workflows/test-heavy-governance.yml` for `workflow_dispatch` using `scripts/platform-test-lane heavy-governance`, with scheduled execution enabled only after the first successful manual validation
- [X] T019 [P] [US3] Implement `.gitea/workflows/test-browser.yml` for `workflow_dispatch` using `scripts/platform-test-lane browser`, with scheduled execution enabled only after the first successful manual validation
- [X] T020 [US3] Update `apps/platform/tests/Support/TestLaneManifest.php` and `apps/platform/tests/Support/TestLaneBudget.php` with `manual` and `scheduled` workflow profiles, governance-contract threshold handling for Heavy Governance, and warning or trend-only modes for Heavy Governance and Browser
- [X] T021 [US3] Update `README.md`, `specs/210-ci-matrix-budget-enforcement/quickstart.md`, and `specs/210-ci-matrix-budget-enforcement/spec.md` with the final trigger policy, local reproduction commands, artifact locations, blocking versus non-blocking guidance for Fast Feedback, Confidence, Heavy Governance, and Browser, and the documented Fast Feedback CI variance tolerance
- [X] T022 [US3] Wire Heavy Governance and Browser artifact staging and upload in `.gitea/workflows/test-heavy-governance.yml`, `.gitea/workflows/test-browser.yml`, and `scripts/platform-test-artifacts` so both lanes publish separate bundles with deterministic names
**Checkpoint**: Heavy Governance and Browser are now visible, reproducible, and isolated from the default fast path.
---
## Phase 6: Polish & Cross-Cutting Concerns
**Purpose**: Validate the full matrix, record rollout evidence, and finish formatting and cleanup.
- [X] T023 Run focused Pest coverage for `apps/platform/tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php`, `apps/platform/tests/Feature/Guards/CiConfidenceWorkflowContractTest.php`, `apps/platform/tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php`, `apps/platform/tests/Feature/Guards/CiLaneFailureClassificationContractTest.php`, `apps/platform/tests/Feature/Guards/FastFeedbackLaneContractTest.php`, `apps/platform/tests/Feature/Guards/ConfidenceLaneContractTest.php`, `apps/platform/tests/Feature/Guards/HeavyGovernanceLaneContractTest.php`, `apps/platform/tests/Feature/Guards/BrowserLaneIsolationTest.php`, `apps/platform/tests/Feature/Guards/FixtureLaneImpactBudgetTest.php`, `apps/platform/tests/Feature/Guards/TestLaneManifestTest.php`, `apps/platform/tests/Feature/Guards/TestLaneArtifactsContractTest.php`, and `apps/platform/tests/Feature/Guards/TestLaneCommandContractTest.php` with `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Guards/CiFastFeedbackWorkflowContractTest.php tests/Feature/Guards/CiConfidenceWorkflowContractTest.php tests/Feature/Guards/CiHeavyBrowserWorkflowContractTest.php tests/Feature/Guards/CiLaneFailureClassificationContractTest.php tests/Feature/Guards/FastFeedbackLaneContractTest.php tests/Feature/Guards/ConfidenceLaneContractTest.php tests/Feature/Guards/HeavyGovernanceLaneContractTest.php tests/Feature/Guards/BrowserLaneIsolationTest.php tests/Feature/Guards/FixtureLaneImpactBudgetTest.php tests/Feature/Guards/TestLaneManifestTest.php tests/Feature/Guards/TestLaneArtifactsContractTest.php tests/Feature/Guards/TestLaneCommandContractTest.php`
- [X] T024 Run local wrapper validation via `./scripts/platform-test-lane fast-feedback`, `./scripts/platform-test-lane confidence`, `./scripts/platform-test-lane heavy-governance`, `./scripts/platform-test-lane browser`, `./scripts/platform-test-report fast-feedback`, and `./scripts/platform-test-report confidence`, then confirm the CI-governance helper and guard changes do not widen default fixtures or promote fast-path coverage into Heavy Governance or Browser lane membership
- [ ] T025 [P] Execute the required live Gitea validation evidence set: one `pull_request` Fast Feedback run, one `push` to `dev` Confidence run, one manual Heavy Governance run, one scheduled Heavy Governance run, one manual Browser run, and one scheduled Browser run using `.gitea/workflows/test-pr-fast-feedback.yml`, `.gitea/workflows/test-main-confidence.yml`, `.gitea/workflows/test-heavy-governance.yml`, and `.gitea/workflows/test-browser.yml`, then record trigger, executed lane, artifact bundle, budget outcome, and the primary failure class or explicit clean-success result in `specs/210-ci-matrix-budget-enforcement/quickstart.md`, and record the chosen Fast Feedback variance tolerance and any material runtime recalibration note in `specs/210-ci-matrix-budget-enforcement/spec.md` or the active PR
- [X] T026 Run `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` for PHP changes in `apps/platform/tests/Support/TestLaneManifest.php`, `apps/platform/tests/Support/TestLaneBudget.php`, `apps/platform/tests/Support/TestLaneReport.php`, and the new guard tests under `apps/platform/tests/Feature/Guards/`, then manually normalize shell and YAML formatting in `scripts/platform-test-artifacts` and `.gitea/workflows/*.yml`
---
## Dependencies & Execution Order
### Phase Dependencies
- **Setup (Phase 1)**: No dependencies and can start immediately.
- **Foundational (Phase 2)**: Depends on Phase 1 and blocks all user story work.
- **User Story 1 (Phase 3)**: Depends on Phase 2 only.
- **User Story 2 (Phase 4)**: Depends on Phase 2 only.
- **User Story 3 (Phase 5)**: Depends on Phase 2 for workflow implementation; final contributor guidance and full-matrix validation should wait until User Stories 1 and 2 are in place.
- **Polish (Phase 6)**: Depends on all desired user stories being complete.
### User Story Dependencies
- **User Story 1 (P1)**: Can begin immediately after Foundational and is the MVP slice.
- **User Story 2 (P1)**: Can begin immediately after Foundational and remains independently valuable even if Heavy Governance and Browser are not yet wired.
- **User Story 3 (P2)**: Uses the same shared seams as User Stories 1 and 2 and should finish after them so the final documented trigger matrix is complete.
### Within Each User Story
- Story-specific guard tests must be written and fail before implementation.
- Workflow metadata in `TestLaneManifest.php` and policy logic in `TestLaneBudget.php` should be in place before finalizing the matching workflow YAML.
- Artifact upload wiring should happen after the workflow file and lane policy both exist.
- Story validation should complete before moving on to the next priority slice.
### Parallel Opportunities
- T001 and T002 can run in parallel during Setup.
- T004, T005, and T006 can run in parallel once T003 defines the shared manifest shape.
- User Stories 1 and 2 can proceed in parallel after Foundational if different contributors own the workflow YAML and guard files.
- In User Story 3, T018 and T019 can run in parallel because Heavy Governance and Browser use separate workflow files.
- T025 can run in parallel with final formatting only after all workflows and guards are stable.
---
## Parallel Example: User Story 1
```bash
# After T008 defines the PR contract, these can proceed in parallel:
Task: "Implement .gitea/workflows/test-pr-fast-feedback.yml for pull_request events using scripts/platform-test-lane fast-feedback"
Task: "Update apps/platform/tests/Support/TestLaneManifest.php and apps/platform/tests/Support/TestLaneBudget.php with the pr-fast-feedback workflow profile and hard-fail policy"
```
---
## Parallel Example: User Story 2
```bash
# After T012 locks the Confidence workflow contract, these can proceed in parallel:
Task: "Implement .gitea/workflows/test-main-confidence.yml for push to dev using scripts/platform-test-lane confidence"
Task: "Update apps/platform/tests/Support/TestLaneManifest.php with the main-confidence workflow profile and Confidence artifact publication contract"
```
---
## Parallel Example: User Story 3
```bash
# After T017 defines the heavy/browser workflow contract, these can proceed in parallel:
Task: "Implement .gitea/workflows/test-heavy-governance.yml for workflow_dispatch and scheduled execution"
Task: "Implement .gitea/workflows/test-browser.yml for workflow_dispatch and scheduled execution"
```
---
## Implementation Strategy
### MVP First (User Story 1 Only)
1. Complete Phase 1: Setup.
2. Complete Phase 2: Foundational.
3. Complete Phase 3: User Story 1.
4. Stop and validate the pull request fast path independently.
### Incremental Delivery
1. Deliver the blocking pull request path first.
2. Add the broader `dev` Confidence path next.
3. Add Heavy Governance and Browser as separate scheduled or manual lanes.
4. Finish with live validation evidence and final contributor guidance.
### Parallel Team Strategy
1. One contributor extends the shared support seams in `apps/platform/tests/Support/` while another prepares workflow YAML files.
2. After Foundational completes, separate owners can implement the PR and `dev` workflows in parallel.
3. Heavy Governance and Browser can then be split across two contributors because they live in separate workflow files.
---
## Notes
- `[P]` tasks operate on different files and can run in parallel once their dependencies are satisfied.
- `[US1]`, `[US2]`, and `[US3]` map each task to the corresponding user story in `spec.md`.
- This feature changes runtime validation behavior, so focused Pest guards and the narrowest relevant lane reruns are part of the definition of done.
- Live Gitea validation is required because local wrapper tests alone cannot prove trigger behavior or uploaded artifact bundles.