Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Darrazi
49fe4626e8 Spec 208: finalize heavy suite segmentation 2026-04-17 11:44:04 +02:00
60 changed files with 368 additions and 6849 deletions

View File

@ -1,74 +0,0 @@
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

@ -1,74 +0,0 @@
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

@ -1,62 +0,0 @@
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

@ -1,64 +0,0 @@
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

@ -195,9 +195,6 @@ ## Active Technologies
- 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, 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)
@ -232,8 +229,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions PHP 8.4.15: Follow standard conventions
## Recent Changes ## Recent Changes
- 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
- 209-heavy-governance-cost: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
- 208-heavy-suite-segmentation: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail - 208-heavy-suite-segmentation: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
- 207-shared-test-fixture-slimming: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
- 206-test-suite-governance: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Pest Browser plugin, Filament v5, Livewire v4, Laravel Sail
<!-- MANUAL ADDITIONS START --> <!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END --> <!-- MANUAL ADDITIONS END -->

View File

@ -10,6 +10,5 @@ ## 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,32 +1,37 @@
<!-- <!--
Sync Impact Report Sync Impact Report
- Version change: 2.3.0 -> 2.4.0 - Version change: 2.2.0 -> 2.3.0
- Modified principles: - Modified principles:
- Quality Gates: expanded to require narrowest-lane validation and - UI-CONST-001: expanded to make TenantPilot's decision-first
runtime-drift notes for runtime changes governance identity explicit
- Governance review expectations: expanded to make lane/runtime - UI-REVIEW-001: spec and PR review gates expanded for surface role,
impact a mandatory part of spec and PR review human-in-the-loop justification, workflow-vs-storage IA, and
attention-load reduction
- Immediate Retrofit Priorities: expanded with a classification-first
wave for existing surfaces
- Added sections: - Added sections:
- Test Suite Governance Must Live In The Delivery Workflow - Decision-First Operating Model & Progressive Disclosure
(TEST-GOV-001) (DECIDE-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 (test-governance planning and - ✅ .specify/templates/plan-template.md (Constitution Check updated for
lane-impact checks added) decision-first surface roles, workflow-first IA, and calm-surface
- ✅ .specify/templates/spec-template.md (mandatory testing/lane/runtime review)
impact section added) - ✅ .specify/templates/spec-template.md (surface role classification,
- ✅ .specify/templates/tasks-template.md (lane classification, operator contract, and requirements updated for decision-first
fixture-cost, and runtime-drift task guidance added) governance)
- ✅ .specify/templates/checklist-template.md (runtime checklist note - ✅ .specify/templates/tasks-template.md (implementation task guidance
added) updated for progressive disclosure, single-case context, and
- ✅ .specify/README.md (SpecKit workflow note added for lane/runtime attention-load reduction)
ownership) - ✅ docs/product/standards/README.md (Constitution index updated for
- ✅ README.md (developer routine updated for test-governance upkeep) DECIDE-001)
- 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: None - Follow-up TODOs:
- Create a dedicated surface / IA classification spec to retrofit
existing surfaces against DECIDE-001.
--> -->
# TenantPilot Constitution # TenantPilot Constitution
@ -102,13 +107,6 @@ ### 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.
@ -1328,7 +1326,6 @@ ### 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
@ -1337,11 +1334,9 @@ ### 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.
@ -1355,4 +1350,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.4.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-17 **Version**: 2.3.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-04-12

View File

@ -5,7 +5,6 @@ # [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,7 +21,6 @@ ## 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]
@ -49,7 +48,6 @@ ## 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
@ -89,18 +87,6 @@ ## 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,18 +88,6 @@ ## 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)*
<!-- <!--
@ -187,13 +175,6 @@ ## 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,12 +9,6 @@ # 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).
@ -129,7 +123,6 @@ # 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,26 +64,6 @@ ### 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

@ -4,6 +4,9 @@
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;
@ -13,7 +16,22 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
[$profile] = seedActiveBaselineForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -32,7 +50,22 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
seedActiveBaselineForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -50,7 +83,22 @@
[$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly'); [$readonlyUser, $tenant] = createUserWithTenant(role: 'readonly');
[$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner'); [$ownerUser] = createUserWithTenant(tenant: $tenant, role: 'owner');
[$profile] = seedActiveBaselineForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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,7 +7,6 @@
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;
@ -31,21 +30,12 @@ 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 = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -57,7 +47,9 @@ function seedCaptureProfileForTenant(
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
[$otherUser, $otherTenant] = createUserWithTenant(role: 'owner'); [$otherUser, $otherTenant] = createUserWithTenant(role: 'owner');
$profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $otherTenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $otherTenant->workspace_id);
@ -71,7 +63,9 @@ function seedCaptureProfileForTenant(
[$user, $tenant] = createUserWithTenant(role: 'readonly'); [$user, $tenant] = createUserWithTenant(role: 'readonly');
$profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
]);
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
@ -92,7 +86,10 @@ function seedCaptureProfileForTenant(
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = seedCaptureProfileForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -132,7 +129,10 @@ function seedCaptureProfileForTenant(
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = seedCaptureProfileForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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,7 +152,8 @@ function seedCaptureProfileForTenant(
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 = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic, [ $profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $tenant->workspace_id,
'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []], 'scope_jsonb' => ['policy_types' => ['deviceConfiguration'], 'foundation_types' => []],
]); ]);
@ -171,7 +172,9 @@ function seedCaptureProfileForTenant(
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$profile = seedCaptureProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'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,7 +8,6 @@
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;
@ -35,27 +34,29 @@ 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] = seedComparableBaselineProfileForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -76,7 +77,23 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
[$profile] = seedComparableBaselineProfileForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -106,7 +123,23 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
[$profile] = seedComparableBaselineProfileForTenant($tenant); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -134,9 +167,7 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
app(FakeCompareStrategy::class), app(FakeCompareStrategy::class),
])); ]));
[$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
$profile->forceFill([
'workspace_id' => (int) $tenant->workspace_id, 'workspace_id' => (int) $tenant->workspace_id,
'scope_jsonb' => [ 'scope_jsonb' => [
'version' => 2, 'version' => 2,
@ -155,7 +186,20 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
], ],
], ],
], ],
])->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);
@ -174,7 +218,22 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
$this->actingAs($user); $this->actingAs($user);
[$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -209,7 +268,22 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
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] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -225,11 +299,23 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
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] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'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);
@ -246,7 +332,22 @@ function seedComparableBaselineProfileForTenant(Tenant $tenant, BaselineCaptureM
[$user, $tenant] = createUserWithTenant(role: 'owner'); [$user, $tenant] = createUserWithTenant(role: 'owner');
[$profile] = seedComparableBaselineProfileForTenant($tenant, BaselineCaptureMode::Opportunistic); $profile = BaselineProfile::factory()->active()->create([
'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,41 +12,29 @@
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(6) ->count(101)
->for($tenant) ->for($tenant)
->create([ ->create([
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
'triaged_at' => null, 'triaged_at' => null,
]); ]);
$component = Livewire::test(ListFindings::class); 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(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.triaged')) expect(AuditLog::query()
->toEqual($findings->pluck('id')->sort()->values()->all()); ->where('tenant_id', (int) $tenant->getKey())
->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');
@ -60,7 +48,7 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
'owner_user_id' => null, 'owner_user_id' => null,
]); ]);
$component Livewire::test(ListFindings::class)
->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(),
@ -74,8 +62,10 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey()); ->and((int) $finding->owner_user_id)->toBe((int) $manager->getKey());
}); });
expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.assigned')) expect(AuditLog::query()
->toEqual($assignFindings->pluck('id')->sort()->values()->all()); ->where('tenant_id', (int) $tenant->getKey())
->where('action', 'finding.assigned')
->count())->toBe(3);
$resolveFindings = Finding::factory() $resolveFindings = Finding::factory()
->count(2) ->count(2)
@ -86,7 +76,7 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
'resolved_reason' => null, 'resolved_reason' => null,
]); ]);
$component Livewire::test(ListFindings::class)
->callTableBulkAction('resolve_selected', $resolveFindings, data: [ ->callTableBulkAction('resolve_selected', $resolveFindings, data: [
'resolved_reason' => 'fixed', 'resolved_reason' => 'fixed',
]) ])
@ -100,8 +90,10 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
->and($finding->resolved_at)->not->toBeNull(); ->and($finding->resolved_at)->not->toBeNull();
}); });
expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.resolved')) expect(AuditLog::query()
->toEqual($resolveFindings->pluck('id')->sort()->values()->all()); ->where('tenant_id', (int) $tenant->getKey())
->where('action', 'finding.resolved')
->count())->toBe(2);
$closeFindings = Finding::factory() $closeFindings = Finding::factory()
->count(2) ->count(2)
@ -112,7 +104,7 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
'closed_reason' => null, 'closed_reason' => null,
]); ]);
$component Livewire::test(ListFindings::class)
->callTableBulkAction('close_selected', $closeFindings, data: [ ->callTableBulkAction('close_selected', $closeFindings, data: [
'closed_reason' => 'not applicable', 'closed_reason' => 'not applicable',
]) ])
@ -127,6 +119,9 @@ function findingBulkAuditResourceIds(int $tenantId, string $action): array
->and($finding->closed_by_user_id)->not->toBeNull(); ->and($finding->closed_by_user_id)->not->toBeNull();
}); });
expect(findingBulkAuditResourceIds((int) $tenant->getKey(), 'finding.closed')) expect(AuditLog::query()
->toEqual($closeFindings->pluck('id')->sort()->values()->all()); ->where('tenant_id', (int) $tenant->getKey())
->where('action', 'finding.closed')
->count())->toBe(2);
}); });

View File

@ -19,38 +19,6 @@
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();
@ -188,7 +156,29 @@ function seedApprovedFindingExceptionWindow(): array
}); });
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, $approver, $tenant, $finding, $service, $active] = seedApprovedFindingExceptionWindow(); [$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',
]);
$service->renew($active, $requester, [ $service->renew($active, $requester, [
'owner_user_id' => (int) $requester->getKey(), 'owner_user_id' => (int) $requester->getKey(),

View File

@ -23,9 +23,7 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
$component = Livewire::test(ListFindings::class); Livewire::test(ListFindings::class)
$component
->callTableAction('triage', $finding) ->callTableAction('triage', $finding)
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
@ -33,7 +31,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();
$component Livewire::test(ListFindings::class)
->callTableAction('start_progress', $finding) ->callTableAction('start_progress', $finding)
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
@ -41,7 +39,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();
$component Livewire::test(ListFindings::class)
->callTableAction('resolve', $finding, [ ->callTableAction('resolve', $finding, [
'resolved_reason' => 'patched', 'resolved_reason' => 'patched',
]) ])
@ -52,7 +50,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();
$component Livewire::test(ListFindings::class)
->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.',
@ -78,15 +76,13 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
$component = Livewire::test(ListFindings::class); Livewire::test(ListFindings::class)
$component
->callTableAction('close', $closeFinding, [ ->callTableAction('close', $closeFinding, [
'closed_reason' => 'duplicate ticket', 'closed_reason' => 'duplicate ticket',
]) ])
->assertHasNoTableActionErrors(); ->assertHasNoTableActionErrors();
$component Livewire::test(ListFindings::class)
->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',
@ -121,9 +117,7 @@
'status' => Finding::STATUS_NEW, 'status' => Finding::STATUS_NEW,
]); ]);
$component = Livewire::test(ListFindings::class); 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(),
@ -134,7 +128,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());
$component Livewire::test(ListFindings::class)
->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,9 +54,7 @@
$finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]); $finding = Finding::factory()->for($tenant)->create(['status' => Finding::STATUS_NEW]);
$component = Livewire::test(ViewFinding::class, ['record' => $finding->getKey()]); Livewire::test(ViewFinding::class, ['record' => $finding->getKey()])
$component
->callAction('triage') ->callAction('triage')
->assertHasNoActionErrors() ->assertHasNoActionErrors()
->callAction('assign', [ ->callAction('assign', [

View File

@ -11,16 +11,6 @@
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())
@ -29,7 +19,9 @@ 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 {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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,
@ -53,11 +45,14 @@ 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 {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
[$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,
]); ]);
@ -110,7 +105,9 @@ function findingFilterIndicatorLabels($component): array
}); });
it('filters findings by high severity quick filter', function (): void { it('filters findings by high severity quick filter', function (): void {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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,
@ -134,7 +131,9 @@ 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] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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');
@ -161,7 +160,9 @@ 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 {
actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$this->actingAs($user);
Filament::setTenant($tenant, true);
$component = Livewire::test(ListFindings::class) $component = Livewire::test(ListFindings::class)
->searchTable('drift') ->searchTable('drift')
@ -181,7 +182,9 @@ 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 {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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,
@ -211,7 +214,9 @@ 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 {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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,
@ -245,7 +250,9 @@ 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 {
[, $tenant] = actingAsFindingsManagerForFilters(); [$user, $tenant] = createUserWithTenant(role: 'manager');
$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,19 +126,9 @@
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,7 +4,6 @@
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 and class', function (): void { it('keeps browser tests isolated behind their dedicated lane and class', function (): void {
$lane = TestLaneManifest::lane('browser'); $lane = TestLaneManifest::lane('browser');
@ -44,13 +43,3 @@
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

@ -1,21 +0,0 @@
<?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

@ -1,21 +0,0 @@
<?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

@ -1,45 +0,0 @@
<?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

@ -1,73 +0,0 @@
<?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,7 +3,6 @@
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 moved heavy families', 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');
@ -71,16 +70,3 @@
->and(trim((string) ($family['confidenceRationale'] ?? '')))->not->toBe(''); ->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,7 +3,6 @@
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 with explicit heavy exclusions', 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');
@ -41,16 +40,3 @@
->and($validation['valid'])->toBeFalse() ->and($validation['valid'])->toBeFalse()
->and($validation['resolvedClassificationId'])->toBe('surface-guard'); ->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

@ -55,16 +55,8 @@
it('defines lane, classification, and family budget targets for heavy-governance attribution', function (): void { it('defines lane, classification, and family budget targets for heavy-governance attribution', function (): void {
$budgetTargets = collect(TestLaneManifest::budgetTargets()); $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() expect($budgetTargets->contains(static fn (array $target): bool => $target['targetType'] === 'lane' && $target['targetId'] === 'heavy-governance'))->toBeTrue()
->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'] === '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'] === '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'] === 'action-surface-contract'))->toBeTrue()
@ -72,8 +64,6 @@
}); });
it('evaluates heavy-governance budgets against named class and family totals', function (): void { it('evaluates heavy-governance budgets against named class and family totals', function (): void {
$currentRunContract = TestLaneManifest::heavyGovernanceBudgetContract(110.0);
$durationsByFile = [ $durationsByFile = [
'tests/Feature/Guards/ActionSurfaceContractTest.php' => 31.2, 'tests/Feature/Guards/ActionSurfaceContractTest.php' => 31.2,
'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php' => 17.4, 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php' => 17.4,
@ -105,7 +95,5 @@
->toEqualCanonicalizing(['lane', 'classification', 'family']) ->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'] === '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'] === '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(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,7 +4,6 @@
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest; use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('routes escalated workflow, discovery-heavy, and broad surface-guard families 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');
@ -65,46 +64,3 @@
'tests/Feature/Filament/BackupSetAdminTenantParityTest.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,16 +4,6 @@
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

@ -61,18 +61,3 @@
->and(collect($report['familyAttribution'])->pluck('familyId')->all()) ->and(collect($report['familyAttribution'])->pluck('familyId')->all())
->toContain('baseline-compare-matrix-workflow', 'action-surface-contract', 'backup-set-admin-tenant-parity'); ->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,34 +2,29 @@
declare(strict_types=1); declare(strict_types=1);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneReport; use Tests\Support\TestLaneReport;
function heavyGovernanceSyntheticHotspots(): array function heavyGovernanceSyntheticHotspots(): array
{ {
return [ return [
['file' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php', 'seconds' => 22.4], ['file' => 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'seconds' => 31.2],
['file' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php', 'seconds' => 21.1], ['file' => 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php', 'seconds' => 17.4],
['file' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php', 'seconds' => 19.5], ['file' => 'tests/Feature/Filament/PolicyVersionAdminSearchParityTest.php', 'seconds' => 16.1],
['file' => 'tests/Feature/Findings/FindingBulkActionsTest.php', 'seconds' => 18.2], ['file' => 'tests/Feature/Rbac/BackupItemsRelationManagerUiEnforcementTest.php', 'seconds' => 14.5],
['file' => 'tests/Feature/Findings/FindingWorkflowRowActionsTest.php', 'seconds' => 14.8], ['file' => 'tests/Feature/Rbac/WorkspaceMembershipsRelationManagerUiEnforcementTest.php', 'seconds' => 12.8],
['file' => 'tests/Feature/Findings/FindingWorkflowViewActionsTest.php', 'seconds' => 13.6], ['file' => 'tests/Feature/Filament/TenantReviewHeaderDisciplineTest.php', 'seconds' => 11.7],
['file' => 'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php', 'seconds' => 12.7], ['file' => 'tests/Feature/Filament/PanelNavigationSegregationTest.php', 'seconds' => 10.9],
['file' => 'tests/Feature/Guards/ActionSurfaceContractTest.php', 'seconds' => 11.9], ['file' => 'tests/Feature/Filament/Alerts/AlertsKpiHeaderTest.php', 'seconds' => 9.8],
['file' => 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'seconds' => 10.4], ['file' => 'tests/Feature/Guards/OperationLifecycleOpsUxGuardTest.php', 'seconds' => 8.7],
['file' => 'tests/Feature/Filament/PolicyResourceAdminSearchParityTest.php', 'seconds' => 4.2], ['file' => 'tests/Feature/ProviderConnections/CredentialLeakGuardTest.php', 'seconds' => 7.6],
]; ];
} }
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/');
} }
@ -43,7 +38,7 @@ function heavyGovernanceSyntheticHotspots(): array
->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 { it('publishes heavy attribution and budget payloads under the canonical artifact root', function (): void {
$durationsByFile = collect(heavyGovernanceSyntheticHotspots()) $durationsByFile = collect(heavyGovernanceSyntheticHotspots())
->mapWithKeys(static fn (array $entry): array => [$entry['file'] => $entry['seconds']]) ->mapWithKeys(static fn (array $entry): array => [$entry['file'] => $entry['seconds']])
->all(); ->all();
@ -69,105 +64,15 @@ function heavyGovernanceSyntheticHotspots(): array
expect($report['artifactDirectory'])->toBe('storage/logs/test-lanes') expect($report['artifactDirectory'])->toBe('storage/logs/test-lanes')
->and($report['slowestEntries'])->toHaveCount(10) ->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()) ->and(collect($report['classificationAttribution'])->pluck('classificationId')->all())
->toContain('ui-workflow', 'surface-guard', 'discovery-heavy') ->toContain('surface-guard', 'discovery-heavy')
->and(collect($report['familyAttribution'])->pluck('familyId')->all()) ->and(collect($report['familyAttribution'])->pluck('familyId')->all())
->toContain( ->toContain('action-surface-contract', 'policy-resource-admin-search-parity', 'ops-ux-governance')
'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()) ->and(collect($report['budgetEvaluations'])->pluck('targetType')->unique()->values()->all())
->toEqualCanonicalizing(['lane', 'classification', 'family']) ->toEqualCanonicalizing(['lane', 'classification', 'family'])
->and($report['familyBudgetEvaluations'])->not->toBeEmpty() ->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

@ -33,58 +33,7 @@
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 {

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 and the spec 208 plus 209 metadata surfaces', function (): void { it('declares the six checked-in lanes with a single fast-feedback default and the spec 208 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,9 +12,8 @@
static fn (array $lane): bool => $lane['defaultEntryPoint'] === true, static fn (array $lane): bool => $lane['defaultEntryPoint'] === true,
)); ));
expect($manifest['version'])->toBe(2) expect($manifest['version'])->toBe(1)
->and($manifest['artifactDirectory'])->toBe('storage/logs/test-lanes') ->and($manifest['artifactDirectory'])->toBe('storage/logs/test-lanes')
->and($manifest['mainlineBranch'])->toBe('dev')
->and($manifest)->toHaveKeys([ ->and($manifest)->toHaveKeys([
'classifications', 'classifications',
'families', 'families',
@ -23,19 +22,7 @@
'driftGuards', 'driftGuards',
'budgetTargets', 'budgetTargets',
'lanes', 'lanes',
'workflowProfiles',
'laneBindings',
'budgetEnforcementProfiles',
'artifactPublicationContracts',
'failureClasses',
'familyBudgets', 'familyBudgets',
'heavyGovernanceBudgetContract',
'heavyGovernanceHotspotInventory',
'heavyGovernanceDecompositionRecords',
'heavyGovernanceSlimmingDecisions',
'heavyGovernanceBudgetSnapshots',
'heavyGovernanceBudgetOutcome',
'heavyGovernanceAuthorGuidance',
]) ])
->and($laneIds)->toEqualCanonicalizing([ ->and($laneIds)->toEqualCanonicalizing([
'fast-feedback', 'fast-feedback',
@ -49,37 +36,6 @@
->and($defaultLanes[0]['id'])->toBe('fast-feedback'); ->and($defaultLanes[0]['id'])->toBe('fast-feedback');
}); });
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 { 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('')
@ -158,38 +114,3 @@
->and(collect($familyBudgets)->pluck('familyId')->all()) ->and(collect($familyBudgets)->pluck('familyId')->all())
->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'); ->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

@ -80,21 +80,3 @@
expect(TestLaneManifest::describeFilePlacement('tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php')) expect(TestLaneManifest::describeFilePlacement('tests/Feature/Baselines/BaselineCompareMatrixBuilderTest.php'))
->toContain('baseline-compare-matrix-workflow', 'ui-workflow', 'Mixed-file'); ->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

@ -119,252 +119,4 @@ public static function evaluateBudgetTargets(
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);
}
} }

View File

@ -14,10 +14,6 @@ final class TestLaneManifest
{ {
private const ARTIFACT_DIRECTORY = 'storage/logs/test-lanes'; private const ARTIFACT_DIRECTORY = 'storage/logs/test-lanes';
private const MAINLINE_BRANCH = 'dev';
private const CI_RUNNER_LABEL = 'ubuntu-latest';
private const FULL_SUITE_BASELINE_SECONDS = 2625; private const FULL_SUITE_BASELINE_SECONDS = 2625;
private const COMMAND_REFS = [ private const COMMAND_REFS = [
@ -56,9 +52,8 @@ final class TestLaneManifest
public static function manifest(): array public static function manifest(): array
{ {
return [ return [
'version' => 2, 'version' => 1,
'artifactDirectory' => self::artifactDirectory(), 'artifactDirectory' => self::artifactDirectory(),
'mainlineBranch' => self::mainlineBranch(),
'classifications' => self::classifications(), 'classifications' => self::classifications(),
'families' => self::families(), 'families' => self::families(),
'mixedFileResolutions' => self::mixedFileResolutions(), 'mixedFileResolutions' => self::mixedFileResolutions(),
@ -66,19 +61,7 @@ public static function manifest(): array
'driftGuards' => self::driftGuards(), 'driftGuards' => self::driftGuards(),
'budgetTargets' => self::budgetTargets(), 'budgetTargets' => self::budgetTargets(),
'lanes' => self::lanes(), 'lanes' => self::lanes(),
'workflowProfiles' => self::workflowProfiles(),
'laneBindings' => self::laneBindings(),
'budgetEnforcementProfiles' => TestLaneBudget::enforcementProfiles(),
'artifactPublicationContracts' => self::artifactPublicationContracts(),
'failureClasses' => self::failureClasses(),
'familyBudgets' => self::familyBudgets(), 'familyBudgets' => self::familyBudgets(),
'heavyGovernanceBudgetContract' => self::heavyGovernanceBudgetContract(),
'heavyGovernanceHotspotInventory' => self::heavyGovernanceHotspotInventory(),
'heavyGovernanceDecompositionRecords' => self::heavyGovernanceDecompositionRecords(),
'heavyGovernanceSlimmingDecisions' => self::heavyGovernanceSlimmingDecisions(),
'heavyGovernanceBudgetSnapshots' => self::heavyGovernanceBudgetSnapshots(),
'heavyGovernanceBudgetOutcome' => self::heavyGovernanceBudgetOutcome(),
'heavyGovernanceAuthorGuidance' => self::heavyGovernanceAuthorGuidance(),
]; ];
} }
@ -1027,10 +1010,10 @@ public static function budgetTargets(): array
'budgetId' => 'lane-heavy-governance', 'budgetId' => 'lane-heavy-governance',
'targetType' => 'lane', 'targetType' => 'lane',
'targetId' => 'heavy-governance', 'targetId' => 'heavy-governance',
'thresholdSeconds' => self::recommendedHeavyGovernanceNormalizedThreshold(), 'thresholdSeconds' => 200,
'baselineSource' => 'measured-lane', 'baselineSource' => 'measured-lane',
'enforcement' => 'warn', 'enforcement' => 'warn',
'lifecycleState' => self::heavyGovernanceBudgetContract()['lifecycleState'], 'lifecycleState' => 'documented',
'reviewCadence' => 'refresh when heavy family ownership changes', 'reviewCadence' => 'refresh when heavy family ownership changes',
], ],
[ [
@ -1284,322 +1267,6 @@ public static function familyBudgets(): array
return $familyBudgets; return $familyBudgets;
} }
/**
* @return array<string, mixed>
*/
public static function heavyGovernanceBudgetContract(?float $measuredSeconds = null): array
{
$measuredSeconds ??= self::heavyGovernanceCurrentMeasuredSeconds();
$normalizedThresholdSeconds = self::recommendedHeavyGovernanceNormalizedThreshold($measuredSeconds);
$decisionStatus = $normalizedThresholdSeconds > self::heavyGovernanceSummaryThresholdSeconds()
? 'recalibrated'
: 'recovered';
return [
'laneId' => 'heavy-governance',
'summaryThresholdSeconds' => self::heavyGovernanceSummaryThresholdSeconds(),
'evaluationThresholdSeconds' => self::heavyGovernanceLegacyEvaluationThresholdSeconds(),
'normalizedThresholdSeconds' => $normalizedThresholdSeconds,
'baselineSource' => 'measured-lane',
'enforcementLevel' => 'warn',
'lifecycleState' => $decisionStatus === 'recalibrated' ? 'recalibrated' : 'documented',
'reconciliationRationale' => $decisionStatus === 'recalibrated'
? sprintf(
'Spec 209 removed duplicated workflow fan-out in the baseline-profile and findings-heavy families, but the settled lane still retains intentional surface-guard depth and the workspace settings residual workflow cost; the normalized contract is %.0fs after the honest rerun.',
$normalizedThresholdSeconds,
)
: 'Spec 209 removed enough duplicate workflow work for the heavy-governance lane to recover within the pre-normalization 300s contract.',
'decisionStatus' => $decisionStatus,
];
}
/**
* @return list<array<string, mixed>>
*/
public static function heavyGovernanceHotspotInventory(): array
{
$measuredSecondsByFamily = self::heavyGovernanceBaselineMeasuredSecondsByFamily();
$inventoryMetadata = [
'baseline-profile-start-surfaces' => ['costDriverCategory' => 'workflow-heavy', 'priorityTier' => 'primary', 'status' => 'slimmed'],
'action-surface-contract' => ['costDriverCategory' => 'intentionally-heavy', 'priorityTier' => 'primary', 'status' => 'retained'],
'ops-ux-governance' => ['costDriverCategory' => 'intentionally-heavy', 'priorityTier' => 'primary', 'status' => 'retained'],
'findings-workflow-surfaces' => ['costDriverCategory' => 'workflow-heavy', 'priorityTier' => 'primary', 'status' => 'slimmed'],
'finding-bulk-actions-workflow' => ['costDriverCategory' => 'redundant', 'priorityTier' => 'primary', 'status' => 'slimmed'],
'workspace-settings-slice-management' => ['costDriverCategory' => 'helper-driven', 'priorityTier' => 'primary', 'status' => 'follow-up'],
];
$inventory = [];
foreach ($inventoryMetadata as $familyId => $metadata) {
$family = self::family($familyId);
$budgetTarget = self::budgetTarget('family', $familyId);
$inventory[] = [
'familyId' => $familyId,
'classificationId' => $family['classificationId'],
'purpose' => $family['purpose'],
'measuredSeconds' => round((float) ($measuredSecondsByFamily[$familyId] ?? 0.0), 6),
'hotspotFiles' => $family['hotspotFiles'],
'costDriverCategory' => $metadata['costDriverCategory'],
'priorityTier' => $metadata['priorityTier'],
'currentBudgetSeconds' => isset($budgetTarget['thresholdSeconds']) ? (float) $budgetTarget['thresholdSeconds'] : null,
'status' => $metadata['status'],
];
}
usort($inventory, static fn (array $left, array $right): int => $right['measuredSeconds'] <=> $left['measuredSeconds']);
return $inventory;
}
/**
* @return list<array<string, mixed>>
*/
public static function heavyGovernanceDecompositionRecords(): array
{
return [
[
'familyId' => 'baseline-profile-start-surfaces',
'trustType' => 'workflow-trust',
'requiredBreadth' => 'One capture start surface, one compare start surface, and one authorization matrix on the baseline profile detail page.',
'duplicateWorkSources' => ['repeated-livewire-mounts', 'header-action-gating-matrix', 'duplicate-baseline-fixture-seeding'],
'duplicateWorkEstimateSeconds' => 12.0,
'residualCostSource' => 'family-breadth',
'recommendedAction' => 'centralize-work',
'notes' => 'The slimming pass keeps the capture and compare start-surface trust intact while reducing repeated Livewire page mounts and repeated start-surface gating setup.',
],
[
'familyId' => 'findings-workflow-surfaces',
'trustType' => 'workflow-trust',
'requiredBreadth' => 'Row actions, view-header actions, list-filter persistence, and renewal workflows remain separate trust slices but now share less repeated mounting work.',
'duplicateWorkSources' => ['repeated-livewire-mounts', 'filter-state-persistence', 'audit-fan-out'],
'duplicateWorkEstimateSeconds' => 6.0,
'residualCostSource' => 'family-breadth',
'recommendedAction' => 'centralize-work',
'notes' => 'The row and view workflow tests shed repeated page mounts while keeping state-transition, assignment, and audit guarantees explicit.',
],
[
'familyId' => 'finding-bulk-actions-workflow',
'trustType' => 'workflow-trust',
'requiredBreadth' => 'Bulk triage, assign, resolve, and close still need per-record workflow and audit verification, but the selected-record fan-out no longer needs a triple-digit fixture count.',
'duplicateWorkSources' => ['audit-fan-out', 'duplicate bulk-action setup'],
'duplicateWorkEstimateSeconds' => 20.0,
'residualCostSource' => 'family-breadth',
'recommendedAction' => 'narrow-assertions',
'notes' => 'The family now validates per-record audit fan-out with a representative selected set instead of an unnecessarily large fixture batch.',
],
[
'familyId' => 'action-surface-contract',
'trustType' => 'surface-trust',
'requiredBreadth' => 'The contract still needs broad action-surface discovery across resources, pages, and relation managers.',
'duplicateWorkSources' => ['resource-discovery-pass', 'surface-wide validation'],
'residualCostSource' => 'intentional-depth',
'recommendedAction' => 'retain-as-heavy',
'notes' => 'No repeatable duplication outweighed the intentionally broad governance surface, so the family remains heavy by design.',
],
[
'familyId' => 'ops-ux-governance',
'trustType' => 'surface-trust',
'requiredBreadth' => 'Ops UX still spans multiple monitoring, notification, and run-detail surfaces with legitimate governance depth.',
'duplicateWorkSources' => ['surface-wide validation', 'cross-surface workflow coverage'],
'residualCostSource' => 'intentional-depth',
'recommendedAction' => 'retain-as-heavy',
'notes' => 'The audit found real breadth rather than a removable duplicate pass, so the family is explicitly retained as intentional heavy coverage.',
],
[
'familyId' => 'workspace-settings-slice-management',
'trustType' => 'workflow-trust',
'requiredBreadth' => 'The settings surface still needs multi-slice save and reset verification, but its residual cost is mostly service and resolver fan-out rather than broad UI discovery.',
'duplicateWorkSources' => ['post-write resolver verification', 'multi-slice form workflow'],
'residualCostSource' => 'helper-driven',
'recommendedAction' => 'route-follow-up',
'notes' => 'Spec 209 records the residual settings cost explicitly instead of disguising it as a family-width win.',
],
];
}
/**
* @return list<array<string, mixed>>
*/
public static function heavyGovernanceSlimmingDecisions(): array
{
return [
[
'familyId' => 'baseline-profile-start-surfaces',
'decisionType' => 'centralize',
'scope' => [
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
],
'guardPreservationPlan' => 'Keep capture and compare launch authorization, rollout gating, and invalid-scope rejection on the real profile detail page while trimming repeated mount overhead.',
'expectedDeltaSeconds' => 12.0,
'owner' => 'platform-test-governance',
'validationPlan' => [
'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php',
'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php',
'tests/Feature/Filament/BaselineActionAuthorizationTest.php',
'scripts/platform-test-lane heavy-governance',
],
],
[
'familyId' => 'findings-workflow-surfaces',
'decisionType' => 'centralize',
'scope' => [
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
],
'guardPreservationPlan' => 'Preserve workflow-state, filter-persistence, and renewal evidence assertions while reducing repeated component bootstraps.',
'expectedDeltaSeconds' => 6.0,
'owner' => 'platform-test-governance',
'validationPlan' => [
'tests/Feature/Findings/FindingWorkflowRowActionsTest.php',
'tests/Feature/Findings/FindingWorkflowViewActionsTest.php',
'tests/Feature/Findings/FindingsListFiltersTest.php',
'tests/Feature/Findings/FindingExceptionRenewalTest.php',
],
],
[
'familyId' => 'finding-bulk-actions-workflow',
'decisionType' => 'trim-duplicate-work',
'scope' => ['tests/Feature/Findings/FindingBulkActionsTest.php'],
'guardPreservationPlan' => 'Keep per-record workflow transitions and audit assertions for each bulk action while cutting the oversized selected-record fan-out.',
'expectedDeltaSeconds' => 20.0,
'owner' => 'platform-test-governance',
'validationPlan' => [
'tests/Feature/Findings/FindingBulkActionsTest.php',
'scripts/platform-test-lane heavy-governance',
],
],
[
'familyId' => 'action-surface-contract',
'decisionType' => 'retain',
'guardPreservationPlan' => 'Retain the broad action-surface discovery contract until a future change proves a genuinely duplicate discovery pass.',
'owner' => 'platform-test-governance',
'validationPlan' => ['tests/Feature/Guards/ActionSurfaceContractTest.php'],
],
[
'familyId' => 'ops-ux-governance',
'decisionType' => 'retain',
'guardPreservationPlan' => 'Keep the broad Ops UX governance surface intact because the current cost comes from intentional coverage breadth, not an accidental duplicate loop.',
'owner' => 'platform-test-governance',
'validationPlan' => [
'tests/Feature/OpsUx/OperationCatalogCoverageTest.php',
'tests/Feature/OpsUx/OperateHubShellTest.php',
'tests/Feature/OpsUx/ActiveRunsTest.php',
],
],
[
'familyId' => 'workspace-settings-slice-management',
'decisionType' => 'follow-up',
'scope' => [
'tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php',
'tests/Support/TestLaneManifest.php',
],
'guardPreservationPlan' => 'Keep the current multi-slice save and reset assertions intact while recording the residual helper-driven cost for later follow-up.',
'owner' => 'platform-test-governance',
'validationPlan' => ['tests/Feature/SettingsFoundation/WorkspaceSettingsManageTest.php'],
],
];
}
/**
* @return list<array<string, mixed>>
*/
public static function heavyGovernanceBudgetSnapshots(): array
{
return [
self::seededHeavyGovernanceBaselineSnapshot(),
self::currentHeavyGovernanceSnapshot(),
];
}
/**
* @return array<string, mixed>
*/
public static function heavyGovernanceBudgetOutcome(): array
{
$snapshots = self::heavyGovernanceBudgetSnapshots();
$contract = self::heavyGovernanceBudgetContract();
$remainingOpenFamilies = array_values(array_map(
static fn (array $record): string => $record['familyId'],
array_filter(
self::heavyGovernanceHotspotInventory(),
static fn (array $record): bool => in_array($record['status'], ['retained', 'follow-up'], true),
),
));
$followUpDebt = array_values(array_map(
static fn (array $decision): string => $decision['familyId'],
array_filter(
self::heavyGovernanceSlimmingDecisions(),
static fn (array $decision): bool => in_array($decision['decisionType'], ['retain', 'follow-up'], true),
),
));
$justification = $contract['decisionStatus'] === 'recalibrated'
? sprintf(
'The workflow-heavy hotspot families were narrowed, but the honest lane still retains intentional surface-guard depth and the workspace settings residual helper cost, so the normalized heavy-governance threshold is %.0fs.',
$contract['normalizedThresholdSeconds'],
)
: 'The baseline-profile and findings-heavy families recovered enough duplicated workflow cost for the heavy-governance lane to fit inside the authoritative 300s threshold.';
return TestLaneBudget::buildOutcomeRecord(
contract: $contract,
baselineSnapshot: $snapshots[0],
currentSnapshot: $snapshots[1],
remainingOpenFamilies: $remainingOpenFamilies,
justification: $justification,
followUpDebt: $followUpDebt,
);
}
/**
* @return list<array<string, mixed>>
*/
public static function heavyGovernanceAuthorGuidance(): array
{
return [
[
'ruleId' => 'heavy-family-reuse-before-creation',
'whenToUse' => 'A new heavy-governance test touches a known baseline, findings, settings, or surface-guard family.',
'requiredDecision' => 'Decide whether the new test belongs in the existing family before creating a new family id.',
'antiPattern' => 'Creating a new heavy family for a scenario that only extends an existing trust boundary.',
'preferredOutcome' => 'Reuse the existing family and update its hotspot or decomposition notes when the trust type stays the same.',
],
[
'ruleId' => 'heavy-family-create-only-for-new-trust',
'whenToUse' => 'A proposed heavy test introduces a trust boundary that is not already represented in the canonical family inventory.',
'requiredDecision' => 'Explain the new trust type, hotspot files, and lane rationale before adding the family.',
'antiPattern' => 'Adding a vague catch-all family without a stable trust description and hotspot inventory entry.',
'preferredOutcome' => 'Create a new family only when the trust boundary and hotspot ownership are both new and reviewable.',
],
[
'ruleId' => 'split-discovery-workflow-surface-concerns',
'whenToUse' => 'A test mixes discovery, workflow, and surface-discipline assertions inside one heavy file.',
'requiredDecision' => 'Separate discovery-trust, workflow-trust, and surface-trust when the same setup is proving unrelated governance rules.',
'antiPattern' => 'One heavy test that silently becomes a catch-all for unrelated trust types.',
'preferredOutcome' => 'Keep each heavy family anchored to one dominant trust type, with any unavoidable secondary cost called out explicitly.',
],
[
'ruleId' => 'retain-intentional-heavy-depth-explicitly',
'whenToUse' => 'A heavy family stays expensive after duplicate work has been removed.',
'requiredDecision' => 'Record that the family is intentionally heavy and explain the remaining governance breadth.',
'antiPattern' => 'Continuing to treat intentional heavy coverage as unexplained budget drift.',
'preferredOutcome' => 'Mark the family as retained with an intentional-depth rationale and validate it with a focused guard suite.',
],
[
'ruleId' => 'record-helper-or-fixture-residuals',
'whenToUse' => 'A hotspot is dominated by helper, resolver, or fixture cost rather than broad UI trust.',
'requiredDecision' => 'Record the residual helper or fixture cause instead of pretending the family itself was fully slimmed.',
'antiPattern' => 'Claiming a family-width win when the remaining cost is actually outside the family boundary.',
'preferredOutcome' => 'Route the residual helper or fixture cost into follow-up debt while keeping the family inventory honest.',
],
];
}
/** /**
* @return array<string, mixed>|null * @return array<string, mixed>|null
*/ */
@ -1797,10 +1464,10 @@ public static function lanes(): array
], ],
'artifacts' => ['summary', 'junit-xml', 'budget-report'], 'artifacts' => ['summary', 'junit-xml', 'budget-report'],
'budget' => [ 'budget' => [
'thresholdSeconds' => self::recommendedHeavyGovernanceNormalizedThreshold(), 'thresholdSeconds' => 300,
'baselineSource' => 'measured-lane', 'baselineSource' => 'measured-lane',
'enforcement' => 'warn', 'enforcement' => 'warn',
'lifecycleState' => self::heavyGovernanceBudgetContract()['lifecycleState'], 'lifecycleState' => 'documented',
'reviewCadence' => 'tighten after two stable runs', 'reviewCadence' => 'tighten after two stable runs',
], ],
'dbStrategy' => [ 'dbStrategy' => [
@ -1891,349 +1558,6 @@ public static function lanes(): array
]; ];
} }
public static function mainlineBranch(): string
{
return self::MAINLINE_BRANCH;
}
public static function ciRunnerLabel(): string
{
return self::CI_RUNNER_LABEL;
}
/**
* @return list<array<string, mixed>>
*/
public static function workflowProfiles(): array
{
return [
[
'workflowId' => 'pr-fast-feedback',
'filePath' => '.gitea/workflows/test-pr-fast-feedback.yml',
'triggerClass' => 'pull-request',
'gitEvents' => ['pull_request'],
'branchFilters' => [],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => true,
'scheduleCron' => null,
'laneBindings' => ['fast-feedback'],
],
[
'workflowId' => 'main-confidence',
'filePath' => '.gitea/workflows/test-main-confidence.yml',
'triggerClass' => 'mainline-push',
'gitEvents' => ['push'],
'branchFilters' => [self::mainlineBranch()],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => true,
'scheduleCron' => null,
'laneBindings' => ['confidence'],
],
[
'workflowId' => 'heavy-governance-manual',
'filePath' => '.gitea/workflows/test-heavy-governance.yml',
'triggerClass' => 'manual',
'gitEvents' => ['workflow_dispatch'],
'branchFilters' => [],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => false,
'scheduleCron' => null,
'laneBindings' => ['heavy-governance'],
],
[
'workflowId' => 'heavy-governance-scheduled',
'filePath' => '.gitea/workflows/test-heavy-governance.yml',
'triggerClass' => 'scheduled',
'gitEvents' => ['schedule'],
'branchFilters' => [],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => false,
'scheduleCron' => '17 4 * * 1-5',
'laneBindings' => ['heavy-governance'],
],
[
'workflowId' => 'browser-manual',
'filePath' => '.gitea/workflows/test-browser.yml',
'triggerClass' => 'manual',
'gitEvents' => ['workflow_dispatch'],
'branchFilters' => [],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => false,
'scheduleCron' => null,
'laneBindings' => ['browser'],
],
[
'workflowId' => 'browser-scheduled',
'filePath' => '.gitea/workflows/test-browser.yml',
'triggerClass' => 'scheduled',
'gitEvents' => ['schedule'],
'branchFilters' => [],
'runnerLabel' => self::ciRunnerLabel(),
'blockingDefault' => false,
'scheduleCron' => '43 4 * * 1-5',
'laneBindings' => ['browser'],
],
];
}
/**
* @return array<string, mixed>
*/
public static function workflowProfile(string $workflowId): array
{
foreach (self::workflowProfiles() as $workflowProfile) {
if ($workflowProfile['workflowId'] === $workflowId) {
return $workflowProfile;
}
}
throw new InvalidArgumentException(sprintf('Unknown workflow profile [%s].', $workflowId));
}
/**
* @return list<array<string, mixed>>
*/
public static function workflowProfilesForLane(string $laneId): array
{
return array_values(array_filter(
self::workflowProfiles(),
static fn (array $workflowProfile): bool => in_array($laneId, $workflowProfile['laneBindings'], true),
));
}
/**
* @return list<array<string, mixed>>
*/
public static function laneBindings(): array
{
return array_map(
static function (array $lane): array {
$laneId = (string) $lane['id'];
$artifactContract = self::artifactPublicationContract($laneId);
return [
'laneId' => $laneId,
'executionWrapper' => 'scripts/platform-test-lane',
'reportWrapper' => 'scripts/platform-test-report',
'commandRef' => self::commandRef($laneId),
'governanceClass' => (string) $lane['governanceClass'],
'parallelMode' => (string) $lane['parallelMode'],
'requiredArtifacts' => $artifactContract['requiredFiles'],
'optionalArtifacts' => $artifactContract['optionalFiles'] ?? [],
'artifactExportProfile' => sprintf('%s-export', $laneId),
];
},
self::lanes(),
);
}
/**
* @return array<string, mixed>
*/
public static function laneBinding(string $laneId): array
{
foreach (self::laneBindings() as $laneBinding) {
if ($laneBinding['laneId'] === $laneId) {
return $laneBinding;
}
}
throw new InvalidArgumentException(sprintf('Unknown lane binding [%s].', $laneId));
}
/**
* @return list<array<string, mixed>>
*/
public static function artifactPublicationContracts(): array
{
return array_map(
static fn (array $lane): array => self::artifactPublicationContract((string) $lane['id']),
self::lanes(),
);
}
/**
* @return array<string, mixed>
*/
public static function artifactPublicationContract(string $laneId): array
{
self::lane($laneId);
$requiredFiles = ['summary.md', 'budget.json', 'report.json', 'junit.xml'];
$optionalFiles = $laneId === 'profiling' ? ['profile.txt'] : [];
$sourcePatterns = array_map(
static fn (string $artifactFile): string => sprintf('%s-latest.%s', $laneId, $artifactFile),
array_merge($requiredFiles, $optionalFiles),
);
return [
'contractId' => sprintf('%s-artifacts', $laneId),
'laneId' => $laneId,
'sourceDirectory' => self::artifactDirectory(),
'sourcePatterns' => $sourcePatterns,
'requiredFiles' => $requiredFiles,
'optionalFiles' => $optionalFiles,
'stagingDirectory' => sprintf('ci-artifacts/%s', $laneId),
'stagedNamePattern' => '{laneId}.{artifactFile}',
'uploadGroupName' => sprintf('%s-artifacts', $laneId),
'retentionClass' => match ($laneId) {
'fast-feedback' => 'pr-short',
'confidence', 'junit' => 'mainline-medium',
default => 'scheduled-medium',
},
];
}
/**
* @return list<array<string, mixed>>
*/
public static function failureClasses(): array
{
return [
[
'failureClassId' => 'test-failure',
'sourceStep' => 'scripts/platform-test-lane',
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
'summaryLabel' => 'Test failure',
'remediationHint' => 'Inspect the lane output and fix the failing test before rerunning the workflow.',
],
[
'failureClassId' => 'wrapper-failure',
'sourceStep' => 'scripts/platform-test-lane',
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
'summaryLabel' => 'Wrapper or manifest failure',
'remediationHint' => 'Verify the workflow profile, lane binding, and checked-in wrapper entry points still resolve to the intended lane.',
],
[
'failureClassId' => 'budget-breach',
'sourceStep' => 'tests/Support/TestLaneBudget.php',
'blockingOn' => ['pull-request'],
'summaryLabel' => 'Budget breach',
'remediationHint' => 'Review the measured runtime against the documented variance allowance and update the lane or spec evidence if the baseline legitimately changed.',
],
[
'failureClassId' => 'artifact-publication-failure',
'sourceStep' => 'scripts/platform-test-artifacts',
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
'summaryLabel' => 'Artifact publication failure',
'remediationHint' => 'Regenerate the lane report artifacts and confirm the staging helper exported the full summary, budget, report, and JUnit bundle.',
],
[
'failureClassId' => 'infrastructure-failure',
'sourceStep' => 'gitea-runner',
'blockingOn' => ['pull-request', 'mainline-push', 'scheduled', 'manual'],
'summaryLabel' => 'Infrastructure failure',
'remediationHint' => 'Check checkout, dependency bootstrap, container startup, or runner health before rerunning the workflow.',
],
];
}
/**
* @return array<string, mixed>
*/
public static function failureClass(string $failureClassId): array
{
foreach (self::failureClasses() as $failureClass) {
if ($failureClass['failureClassId'] === $failureClassId) {
return $failureClass;
}
}
throw new InvalidArgumentException(sprintf('Unknown failure class [%s].', $failureClassId));
}
/**
* @return array<string, mixed>
*/
public static function validateWorkflowExecution(string $workflowId, string $laneId): array
{
$workflowProfile = self::workflowProfile($workflowId);
$unresolvedEntryPoints = [];
try {
self::laneBinding($laneId);
} catch (InvalidArgumentException) {
$unresolvedEntryPoints[] = 'lane-binding';
}
if (! array_key_exists($laneId, self::COMMAND_REFS)) {
$unresolvedEntryPoints[] = 'command-ref';
}
if (! is_file(self::repoRoot().'/scripts/platform-test-lane')) {
$unresolvedEntryPoints[] = 'lane-runner';
}
if (! is_file(self::repoRoot().'/scripts/platform-test-report')) {
$unresolvedEntryPoints[] = 'report-runner';
}
if (! is_file(self::repoRoot().DIRECTORY_SEPARATOR.$workflowProfile['filePath'])) {
$unresolvedEntryPoints[] = 'workflow-file';
}
try {
self::artifactPublicationContract($laneId);
} catch (InvalidArgumentException) {
$unresolvedEntryPoints[] = 'artifact-contract';
}
$workflowLaneMatched = in_array($laneId, $workflowProfile['laneBindings'], true);
$entryPointResolved = $unresolvedEntryPoints === [];
$valid = $entryPointResolved && $workflowLaneMatched;
return [
'workflowId' => $workflowId,
'expectedLaneIds' => $workflowProfile['laneBindings'],
'executedLaneId' => $laneId,
'entryPointResolved' => $entryPointResolved,
'workflowLaneMatched' => $workflowLaneMatched,
'unresolvedEntryPoints' => $unresolvedEntryPoints,
'valid' => $valid,
'primaryFailureClassId' => $valid ? null : 'wrapper-failure',
];
}
/**
* @return array<string, mixed>
*/
public static function currentCiContext(string $laneId): array
{
$workflowId = getenv('TENANTATLAS_CI_WORKFLOW_ID') ?: null;
$triggerClass = getenv('TENANTATLAS_CI_TRIGGER_CLASS') ?: null;
if (! is_string($workflowId) && ! is_string($triggerClass)) {
return [];
}
$executionValidation = null;
if (is_string($workflowId) && $workflowId !== '') {
try {
$executionValidation = self::validateWorkflowExecution($workflowId, $laneId);
$triggerClass ??= self::workflowProfile($workflowId)['triggerClass'];
} catch (InvalidArgumentException) {
$executionValidation = [
'entryPointResolved' => false,
'workflowLaneMatched' => false,
'primaryFailureClassId' => 'wrapper-failure',
'expectedLaneIds' => [],
'unresolvedEntryPoints' => ['workflow-profile'],
];
}
}
return array_filter([
'workflowId' => is_string($workflowId) && $workflowId !== '' ? $workflowId : null,
'triggerClass' => is_string($triggerClass) && $triggerClass !== '' ? $triggerClass : null,
'entryPointResolved' => $executionValidation['entryPointResolved'] ?? true,
'workflowLaneMatched' => $executionValidation['workflowLaneMatched'] ?? true,
'primaryFailureClassId' => $executionValidation['primaryFailureClassId'] ?? null,
'expectedLaneIds' => $executionValidation['expectedLaneIds'] ?? null,
'unresolvedEntryPoints' => $executionValidation['unresolvedEntryPoints'] ?? null,
], static fn (mixed $value): bool => $value !== null);
}
public static function commandRef(string $laneId): string public static function commandRef(string $laneId): string
{ {
if (! array_key_exists($laneId, self::COMMAND_REFS)) { if (! array_key_exists($laneId, self::COMMAND_REFS)) {
@ -2335,12 +1659,7 @@ public static function runLane(string $laneId): int
$capturedOutput .= $buffer; $capturedOutput .= $buffer;
}); });
TestLaneReport::finalizeLane( TestLaneReport::finalizeLane($laneId, microtime(true) - $startedAt, $capturedOutput);
$laneId,
microtime(true) - $startedAt,
$capturedOutput,
exitCode: $process->getExitCode() ?? 1,
);
return $process->getExitCode() ?? 1; return $process->getExitCode() ?? 1;
} }
@ -2371,7 +1690,6 @@ public static function renderLatestReport(string $laneId, ?string $comparisonPro
laneId: $laneId, laneId: $laneId,
report: $report, report: $report,
profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null, profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null,
exitCode: 0,
); );
echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL; echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
@ -2579,209 +1897,11 @@ public static function describeFilePlacement(string $filePath): string
return $message; return $message;
} }
private static function heavyGovernanceSummaryThresholdSeconds(): float
{
return 300.0;
}
private static function heavyGovernanceLegacyEvaluationThresholdSeconds(): float
{
return 200.0;
}
private static function recommendedHeavyGovernanceNormalizedThreshold(?float $measuredSeconds = null): float
{
$resolvedMeasuredSeconds = round($measuredSeconds ?? self::heavyGovernanceCurrentMeasuredSeconds(), 6);
if ($resolvedMeasuredSeconds <= self::heavyGovernanceSummaryThresholdSeconds()) {
return self::heavyGovernanceSummaryThresholdSeconds();
}
return round((float) (ceil($resolvedMeasuredSeconds / 5.0) * 5.0), 6);
}
private static function heavyGovernanceCurrentMeasuredSeconds(): float
{
$report = self::readJsonArtifact(self::heavyGovernanceArtifactPaths('latest')['report']);
if (is_array($report)) {
return round((float) ($report['wallClockSeconds'] ?? 0.0), 6);
}
return round((float) self::seededHeavyGovernanceBaselineSnapshot()['wallClockSeconds'], 6);
}
/**
* @return array{summary: string, budget: string, report: string}
*/
private static function heavyGovernanceArtifactPaths(string $variant): array
{
$directory = self::artifactDirectory();
$suffix = $variant === 'baseline' ? 'baseline' : 'latest';
return [
'summary' => sprintf('%s/heavy-governance-%s.summary.md', $directory, $suffix),
'budget' => sprintf('%s/heavy-governance-%s.budget.json', $directory, $suffix),
'report' => sprintf('%s/heavy-governance-%s.report.json', $directory, $suffix),
];
}
/**
* @return array<string, mixed>|null
*/
private static function readJsonArtifact(string $relativePath): ?array
{
$absolutePath = self::absolutePath($relativePath);
if (! is_file($absolutePath)) {
return null;
}
$decoded = json_decode((string) file_get_contents($absolutePath), true);
return is_array($decoded) ? $decoded : null;
}
/**
* @return array<string, float>
*/
private static function heavyGovernanceBaselineMeasuredSecondsByFamily(): array
{
$snapshot = self::seededHeavyGovernanceBaselineSnapshot();
$totals = [];
foreach ($snapshot['familyTotals'] as $entry) {
$totals[(string) $entry['familyId']] = round((float) $entry['totalWallClockSeconds'], 6);
}
return $totals;
}
/**
* @return array<string, mixed>
*/
private static function currentHeavyGovernanceSnapshot(): array
{
$fallback = self::seededHeavyGovernanceBaselineSnapshot();
$report = self::readJsonArtifact(self::heavyGovernanceArtifactPaths('latest')['report']);
$measuredSeconds = round((float) ($report['wallClockSeconds'] ?? $fallback['wallClockSeconds']), 6);
$normalizedThresholdSeconds = self::recommendedHeavyGovernanceNormalizedThreshold($measuredSeconds);
if (! is_array($report)) {
return array_merge($fallback, [
'snapshotId' => 'post-slimming',
'artifactPaths' => self::heavyGovernanceArtifactPaths('latest'),
'budgetStatus' => $measuredSeconds <= $normalizedThresholdSeconds ? 'within-budget' : 'warning',
]);
}
return [
'snapshotId' => 'post-slimming',
'capturedAt' => (string) ($report['finishedAt'] ?? gmdate('c')),
'wallClockSeconds' => $measuredSeconds,
'classificationTotals' => array_map(
static fn (array $entry): array => [
'classificationId' => (string) $entry['classificationId'],
'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6),
],
$report['classificationAttribution'] ?? [],
),
'familyTotals' => array_map(
static fn (array $entry): array => [
'familyId' => (string) $entry['familyId'],
'totalWallClockSeconds' => round((float) $entry['totalWallClockSeconds'], 6),
],
$report['familyAttribution'] ?? [],
),
'slowestEntries' => array_map(
static fn (array $entry): array => [
'label' => (string) $entry['label'],
'wallClockSeconds' => round((float) ($entry['wallClockSeconds'] ?? 0.0), 6),
],
$report['slowestEntries'] ?? [],
),
'artifactPaths' => self::heavyGovernanceArtifactPaths('latest'),
'budgetStatus' => $measuredSeconds <= $normalizedThresholdSeconds ? 'within-budget' : 'warning',
];
}
/**
* @return array<string, mixed>
*/
private static function seededHeavyGovernanceBaselineSnapshot(): array
{
return [
'snapshotId' => 'pre-slimming',
'capturedAt' => '2026-04-17T11:00:53+00:00',
'wallClockSeconds' => 318.296962,
'classificationTotals' => [
['classificationId' => 'ui-workflow', 'totalWallClockSeconds' => 190.606431],
['classificationId' => 'surface-guard', 'totalWallClockSeconds' => 106.845887],
['classificationId' => 'discovery-heavy', 'totalWallClockSeconds' => 0.863003],
],
'familyTotals' => [
['familyId' => 'baseline-profile-start-surfaces', 'totalWallClockSeconds' => 98.112193],
['familyId' => 'action-surface-contract', 'totalWallClockSeconds' => 40.841552],
['familyId' => 'ops-ux-governance', 'totalWallClockSeconds' => 38.794861],
['familyId' => 'findings-workflow-surfaces', 'totalWallClockSeconds' => 36.459493],
['familyId' => 'finding-bulk-actions-workflow', 'totalWallClockSeconds' => 26.491446],
['familyId' => 'workspace-settings-slice-management', 'totalWallClockSeconds' => 21.740839],
['familyId' => 'workspace-only-admin-surface-independence', 'totalWallClockSeconds' => 11.639077],
['familyId' => 'panel-navigation-segregation', 'totalWallClockSeconds' => 11.022529],
['familyId' => 'drift-bulk-triage-all-matching', 'totalWallClockSeconds' => 7.80246],
['familyId' => 'backup-items-relation-manager-enforcement', 'totalWallClockSeconds' => 2.280078],
['familyId' => 'tenant-review-header-discipline', 'totalWallClockSeconds' => 1.257656],
['familyId' => 'workspace-memberships-relation-manager-enforcement', 'totalWallClockSeconds' => 1.010134],
['familyId' => 'policy-resource-admin-search-parity', 'totalWallClockSeconds' => 0.439257],
['familyId' => 'policy-version-admin-search-parity', 'totalWallClockSeconds' => 0.423746],
],
'slowestEntries' => [
['label' => 'tests/Feature/Findings/FindingBulkActionsTest.php::it supports bulk workflow actions and audits each record', 'wallClockSeconds' => 26.491446],
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it starts capture successfully for authorized workspace members', 'wallClockSeconds' => 12.373413],
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it starts baseline compare successfully for authorized workspace members', 'wallClockSeconds' => 12.228384],
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it does not start full-content baseline compare when rollout is disabled', 'wallClockSeconds' => 11.204111],
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it does not start full-content capture when rollout is disabled', 'wallClockSeconds' => 11.086623],
['label' => 'tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php::it does not start baseline compare for workspace members missing tenant.sync', 'wallClockSeconds' => 10.659623],
['label' => 'tests/Feature/Filament/BaselineActionAuthorizationTest.php::it keeps baseline capture and compare actions capability-gated on the profile detail page', 'wallClockSeconds' => 10.555709],
['label' => 'tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php::it does not start capture for workspace members missing workspace_baselines.manage', 'wallClockSeconds' => 10.428982],
['label' => 'tests/Feature/Drift/DriftBulkAcknowledgeAllMatchingConfirmationTest.php::triage all matching requires typed confirmation when triaging more than 100 findings', 'wallClockSeconds' => 7.80246],
['label' => 'tests/Feature/Filament/WorkspaceOnlySurfaceTenantIndependenceTest.php::it keeps workspace-only admin surfaces independent from remembered tenant changes', 'wallClockSeconds' => 7.779388],
],
'artifactPaths' => self::heavyGovernanceArtifactPaths('baseline'),
'budgetStatus' => 'warning',
];
}
private static function appRoot(): string private static function appRoot(): string
{ {
return dirname(__DIR__, 2); return dirname(__DIR__, 2);
} }
public static function repoRoot(): string
{
$configuredRoot = getenv('TENANTATLAS_REPO_ROOT');
if (is_string($configuredRoot) && $configuredRoot !== '') {
if (str_starts_with($configuredRoot, DIRECTORY_SEPARATOR)) {
return rtrim($configuredRoot, DIRECTORY_SEPARATOR);
}
$resolvedConfiguredRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.$configuredRoot);
if ($resolvedConfiguredRoot !== false) {
return $resolvedConfiguredRoot;
}
}
$resolvedDefaultRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.'../..');
if ($resolvedDefaultRoot !== false) {
return $resolvedDefaultRoot;
}
return dirname(dirname(self::appRoot()));
}
/** /**
* @param list<string> $targets * @param list<string> $targets
*/ */

View File

@ -24,187 +24,6 @@ public static function artifactPaths(string $laneId, ?string $artifactDirectory
]; ];
} }
/**
* @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>} * @return array{slowestEntries: list<array<string, mixed>>, durationsByFile: array<string, float>}
*/ */
@ -270,31 +89,10 @@ 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);
$heavyGovernanceContract = $laneId === 'heavy-governance'
? TestLaneManifest::heavyGovernanceBudgetContract($wallClockSeconds)
: null;
if (is_array($heavyGovernanceContract)) {
$lane['budget']['thresholdSeconds'] = $heavyGovernanceContract['normalizedThresholdSeconds'];
$lane['budget']['lifecycleState'] = $heavyGovernanceContract['lifecycleState'];
}
$laneBudget = TestLaneBudget::fromArray($lane['budget']); $laneBudget = TestLaneBudget::fromArray($lane['budget']);
$laneBudgetEvaluation = $laneBudget->evaluate($wallClockSeconds); $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 = [];
@ -319,30 +117,12 @@ public static function buildReport(
} }
$attribution = self::buildAttribution($durationsByFile); $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( $budgetEvaluations = TestLaneBudget::evaluateBudgetTargets(
$relevantBudgetTargets, self::relevantBudgetTargets(
$laneId,
$attribution['classificationTotals'],
$attribution['familyTotals'],
),
$wallClockSeconds, $wallClockSeconds,
$attribution['classificationTotals'], $attribution['classificationTotals'],
$attribution['familyTotals'], $attribution['familyTotals'],
@ -362,21 +142,6 @@ static function (array $entry) use ($attribution): array {
$slowestEntries, $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(), '/'), 'artifactDirectory' => trim($artifactDirectory ?? TestLaneManifest::artifactDirectory(), '/'),
@ -396,18 +161,8 @@ classificationAttribution: $attribution['classificationAttribution'],
static fn (array $evaluation): bool => ($evaluation['targetType'] ?? null) === 'family', 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 ($heavyGovernanceContext !== []) {
$report = array_merge($report, $heavyGovernanceContext);
}
if ($laneBudget->baselineDeltaTargetPercent !== null) { if ($laneBudget->baselineDeltaTargetPercent !== null) {
$report['baselineDeltaTargetPercent'] = $laneBudget->baselineDeltaTargetPercent; $report['baselineDeltaTargetPercent'] = $laneBudget->baselineDeltaTargetPercent;
} }
@ -418,14 +173,6 @@ classificationAttribution: $attribution['classificationAttribution'],
$report['sharedFixtureSlimmingComparison'] = $comparison; $report['sharedFixtureSlimmingComparison'] = $comparison;
} }
if ($resolvedCiContext !== []) {
$report['ciContext'] = $resolvedCiContext;
}
if ($ciBudgetEvaluation !== null) {
$report['ciBudgetEvaluation'] = $ciBudgetEvaluation;
}
return $report; return $report;
} }
@ -438,7 +185,6 @@ 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);
@ -449,45 +195,34 @@ public static function writeArtifacts(
self::buildSummaryMarkdown($report), self::buildSummaryMarkdown($report),
); );
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['budget']),
json_encode([
'laneId' => $report['laneId'],
'artifactDirectory' => $report['artifactDirectory'],
'wallClockSeconds' => $report['wallClockSeconds'],
'budgetThresholdSeconds' => $report['budgetThresholdSeconds'],
'budgetBaselineSource' => $report['budgetBaselineSource'],
'budgetEnforcement' => $report['budgetEnforcement'],
'budgetLifecycleState' => $report['budgetLifecycleState'],
'budgetStatus' => $report['budgetStatus'],
'classificationAttribution' => $report['classificationAttribution'],
'familyAttribution' => $report['familyAttribution'],
'budgetEvaluations' => $report['budgetEvaluations'],
'familyBudgetEvaluations' => $report['familyBudgetEvaluations'],
'sharedFixtureSlimmingComparison' => $report['sharedFixtureSlimmingComparison'] ?? null,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['report']),
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
if (is_string($profileOutput) && trim($profileOutput) !== '') { if (is_string($profileOutput) && trim($profileOutput) !== '') {
file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput); file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput);
} }
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['budget']),
json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['report']),
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
$report['artifactPublication'] = self::artifactPublicationStatus($laneId, $artifactDirectory);
$report['ciSummary'] = self::buildCiSummary(
report: $report,
exitCode: $exitCode,
budgetOutcome: is_array($report['ciBudgetEvaluation'] ?? null) ? $report['ciBudgetEvaluation'] : null,
artifactPublicationStatus: $report['artifactPublication'],
entryPointResolved: (bool) ($report['ciContext']['entryPointResolved'] ?? true),
workflowLaneMatched: (bool) ($report['ciContext']['workflowLaneMatched'] ?? true),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['summary']),
self::buildSummaryMarkdown($report),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['budget']),
json_encode(self::budgetPayload($report), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['report']),
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
return $artifactPaths; return $artifactPaths;
} }
@ -499,21 +234,18 @@ public static function finalizeLane(
float $wallClockSeconds, float $wallClockSeconds,
string $capturedOutput = '', string $capturedOutput = '',
?string $comparisonProfile = null, ?string $comparisonProfile = null,
?int $exitCode = 0,
): array { ): 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, exitCode: $exitCode); self::writeArtifacts($laneId, $report, $capturedOutput);
return $report; return $report;
} }
@ -532,47 +264,6 @@ 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'];
@ -756,223 +447,6 @@ 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

@ -1,73 +0,0 @@
#!/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,23 +6,6 @@ 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)
@ -51,48 +34,6 @@ 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}"
if [[ ${#remaining_args[@]} -gt 0 ]]; then exec ./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" -- "$@"
./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,23 +6,6 @@ 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 case "${LANE}" in
fast-feedback|fast|default) fast-feedback|fast|default)
@ -46,45 +29,6 @@ case "${LANE}" in
;; ;;
esac 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}"
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}" exec ./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
if [[ "${CAPTURE_BASELINE}" == true ]]; then
copy_heavy_baseline_artifacts
fi

View File

@ -1,38 +0,0 @@
# 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

@ -1,509 +0,0 @@
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

@ -1,565 +0,0 @@
{
"$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

@ -1,193 +0,0 @@
# 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

@ -1,168 +0,0 @@
# 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

@ -1,151 +0,0 @@
# 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

@ -1,81 +0,0 @@
# 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

@ -1,300 +0,0 @@
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

@ -1,182 +0,0 @@
# 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

@ -1,39 +0,0 @@
# 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

@ -1,317 +0,0 @@
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

@ -1,383 +0,0 @@
{
"$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

@ -1,173 +0,0 @@
# 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

@ -1,188 +0,0 @@
# 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

@ -1,108 +0,0 @@
# 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

@ -1,65 +0,0 @@
# 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

@ -1,236 +0,0 @@
# 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

@ -1,202 +0,0 @@
# 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.