Compare commits

..

1 Commits

Author SHA1 Message Date
Ahmed Darrazi
c61af23827 feat: implement heavy governance cost recovery 2026-04-17 15:15:23 +02:00
38 changed files with 66 additions and 3419 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

@ -196,8 +196,6 @@ ## Active Technologies
- SQLite `:memory:` for the default test environment, isolated PostgreSQL coverage via the existing dedicated suite, and lane-measurement artifacts under the app-root contract path `storage/logs/test-lanes` (207-shared-test-fixture-slimming)
- SQLite `:memory:` for the default test environment, existing lane artifacts under the app-root contract path `storage/logs/test-lanes`, and no new product persistence (208-heavy-suite-segmentation)
- SQLite `:memory:` for the default test environment, mixed database strategy for some heavy-governance families as declared in `TestLaneManifest`, and existing lane artifacts under the app-root contract path `storage/logs/test-lanes` (209-heavy-governance-cost)
- PHP 8.4.15 for repo-truth test governance, Bash for repo-root wrappers, and GitHub-compatible Gitea Actions workflow YAML under `.gitea/workflows/` + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail, Gitea Actions backed by `act_runner`, and the existing `Tests\Support\TestLaneManifest`, `TestLaneBudget`, and `TestLaneReport` seams (210-ci-matrix-budget-enforcement)
- SQLite `:memory:` for default lane execution, filesystem artifacts under the app-root contract path `storage/logs/test-lanes`, checked-in workflow YAML under `.gitea/workflows/`, and no new product database persistence (210-ci-matrix-budget-enforcement)
- PHP 8.4.15 (feat/005-bulk-operations)
@ -232,8 +230,8 @@ ## Code Style
PHP 8.4.15: Follow standard conventions
## 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
- 207-shared-test-fixture-slimming: Added PHP 8.4.15 + Laravel 12, Pest v4, PHPUnit 12, Filament v5, Livewire v4, Laravel Sail
<!-- MANUAL ADDITIONS START -->
<!-- MANUAL ADDITIONS END -->

View File

@ -10,6 +10,5 @@ ## Important
- `plan.md`
- `tasks.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.

View File

@ -1,32 +1,37 @@
<!--
Sync Impact Report
- Version change: 2.3.0 -> 2.4.0
- Version change: 2.2.0 -> 2.3.0
- Modified principles:
- Quality Gates: expanded to require narrowest-lane validation and
runtime-drift notes for runtime changes
- Governance review expectations: expanded to make lane/runtime
impact a mandatory part of spec and PR review
- UI-CONST-001: expanded to make TenantPilot's decision-first
governance identity explicit
- UI-REVIEW-001: spec and PR review gates expanded for surface role,
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:
- Test Suite Governance Must Live In The Delivery Workflow
(TEST-GOV-001)
- Decision-First Operating Model & Progressive Disclosure
(DECIDE-001)
- Removed sections: None
- Templates requiring updates:
- ✅ .specify/memory/constitution.md
- ✅ .specify/templates/plan-template.md (test-governance planning and
lane-impact checks added)
- ✅ .specify/templates/spec-template.md (mandatory testing/lane/runtime
impact section added)
- ✅ .specify/templates/tasks-template.md (lane classification,
fixture-cost, and runtime-drift task guidance added)
- ✅ .specify/templates/checklist-template.md (runtime checklist note
added)
- ✅ .specify/README.md (SpecKit workflow note added for lane/runtime
ownership)
- ✅ README.md (developer routine updated for test-governance upkeep)
- ✅ .specify/templates/plan-template.md (Constitution Check updated for
decision-first surface roles, workflow-first IA, and calm-surface
review)
- ✅ .specify/templates/spec-template.md (surface role classification,
operator contract, and requirements updated for decision-first
governance)
- ✅ .specify/templates/tasks-template.md (implementation task guidance
updated for progressive disclosure, single-case context, and
attention-load reduction)
- ✅ docs/product/standards/README.md (Constitution index updated for
DECIDE-001)
- Commands checked:
- 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
@ -102,13 +107,6 @@ ### Tests Must Protect Business Truth (TEST-TRUTH-001)
- 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.
### 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)
- 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.
@ -1328,7 +1326,6 @@ ### Spec-First Workflow
## Quality Gates
- 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.
## 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.
- 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.
- 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
affected surface under DECIDE-001 and justify any new Primary
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.
- 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.
- **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]
**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]
**Storage**: [if applicable, e.g., PostgreSQL, CoreData, files or N/A]
**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]
**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]
@ -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)
- 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
- 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
- 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
@ -89,18 +87,6 @@ ## Constitution Check
selection actions, navigation, and object actions; risky or rare
actions are grouped and ordered by meaning/frequency/risk; any special
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
### 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]
- **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)*
<!--
@ -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,
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:
- 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`),

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/
**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
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).
@ -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.
**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`

View File

@ -64,26 +64,6 @@ ### Canonical Lane Commands
- `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`.
### 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
| Scope | Wall clock | Budget | Notes |

View File

@ -4,7 +4,6 @@
use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps browser tests isolated behind their dedicated lane and class', function (): void {
$lane = TestLaneManifest::lane('browser');
@ -43,14 +42,4 @@
foreach (TestLaneManifest::discoverFiles('confidence') as $path) {
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);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps confidence broader than fast-feedback while excluding browser and moved heavy families', function (): void {
$lane = TestLaneManifest::lane('confidence');
@ -70,17 +69,4 @@
expect($family['classificationId'])->toBeIn(['ui-light', 'ui-workflow'])
->and(trim((string) ($family['confidenceRationale'] ?? '')))->not->toBe('');
}
});
it('keeps the dev confidence budget warning-first so runtime drift stays visible without blocking on budget alone', function (): void {
$profile = TestLaneBudget::enforcementProfile('confidence', 'mainline-push');
$withinVariance = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 470.0);
$overBudget = TestLaneBudget::evaluateLaneForTrigger('confidence', 'mainline-push', 481.0);
expect($profile['enforcementMode'])->toBe('soft-warn')
->and($profile['effectiveThresholdSeconds'])->toBe(480)
->and($withinVariance['budgetStatus'])->toBe('warning')
->and($withinVariance['blockingStatus'])->toBe('non-blocking-warning')
->and($overBudget['budgetStatus'])->toBe('over-budget')
->and($overBudget['blockingStatus'])->toBe('non-blocking-warning');
});

View File

@ -3,7 +3,6 @@
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('keeps fast-feedback as the default parallel contributor loop with explicit heavy exclusions', function (): void {
$lane = TestLaneManifest::lane('fast-feedback');
@ -40,17 +39,4 @@
->and($fastTargets)->not->toContain('tests/Feature/Guards/ActionSurfaceContractTest.php')
->and($validation['valid'])->toBeFalse()
->and($validation['resolvedClassificationId'])->toBe('surface-guard');
});
it('treats pull-request fast-feedback budget policy as hard-fail only after the documented CI tolerance is exceeded', function (): void {
$profile = TestLaneBudget::enforcementProfile('fast-feedback', 'pull-request');
$withinTolerance = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 210.0);
$overTolerance = TestLaneBudget::evaluateLaneForTrigger('fast-feedback', 'pull-request', 216.0);
expect($profile['enforcementMode'])->toBe('hard-fail')
->and($profile['effectiveThresholdSeconds'])->toBe(215)
->and($withinTolerance['budgetStatus'])->toBe('warning')
->and($withinTolerance['blockingStatus'])->toBe('non-blocking-warning')
->and($overTolerance['budgetStatus'])->toBe('over-budget')
->and($overTolerance['blockingStatus'])->toBe('blocking');
});
});

View File

@ -4,7 +4,6 @@
use Illuminate\Support\Collection;
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneBudget;
it('routes escalated workflow, discovery-heavy, and broad surface-guard families into heavy-governance', function (): void {
$lane = TestLaneManifest::lane('heavy-governance');
@ -94,17 +93,4 @@
'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

@ -2,7 +2,6 @@
declare(strict_types=1);
use Tests\Support\TestLaneManifest;
use Tests\Support\TestLaneReport;
function heavyGovernanceSyntheticHotspots(): array
@ -23,13 +22,9 @@ function heavyGovernanceSyntheticHotspots(): array
it('keeps lane artifact paths app-root relative under storage/logs/test-lanes', function (): void {
$artifacts = TestLaneReport::artifactPaths('fast-feedback');
$artifactContract = TestLaneManifest::artifactPublicationContract('fast-feedback');
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) {
expect($relativePath)->toStartWith('storage/logs/test-lanes/');
}
@ -70,9 +65,6 @@ function heavyGovernanceSyntheticHotspots(): array
expect($report['artifactDirectory'])->toBe('storage/logs/test-lanes')
->and($report['slowestEntries'])->toHaveCount(10)
->and($report)->toHaveKeys([
'artifactPublicationContract',
'knownWorkflowProfiles',
'failureClasses',
'budgetContract',
'hotspotInventory',
'decompositionRecords',
@ -106,68 +98,9 @@ function heavyGovernanceSyntheticHotspots(): array
'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 {
$fastFeedback = TestLaneReport::buildReport(
laneId: 'fast-feedback',

View File

@ -33,8 +33,7 @@
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()
->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue()
->and(file_exists(repo_path('scripts/platform-test-artifacts')))->toBeTrue();
->and(file_exists(repo_path('scripts/platform-test-report')))->toBeTrue();
});
it('keeps heavy-governance baseline capture support inside the checked-in wrappers', function (): void {
@ -51,40 +50,7 @@
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));
}
->and($laneRunner)->toContain('./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"');
});
it('routes the foundational lane commands through stable artisan arguments', function (): void {

View File

@ -12,9 +12,8 @@
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['mainlineBranch'])->toBe('dev')
->and($manifest)->toHaveKeys([
'classifications',
'families',
@ -23,11 +22,6 @@
'driftGuards',
'budgetTargets',
'lanes',
'workflowProfiles',
'laneBindings',
'budgetEnforcementProfiles',
'artifactPublicationContracts',
'failureClasses',
'familyBudgets',
'heavyGovernanceBudgetContract',
'heavyGovernanceHotspotInventory',
@ -49,37 +43,6 @@
->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 {
foreach (TestLaneManifest::manifest()['lanes'] as $lane) {
expect(trim($lane['description']))->not->toBe('')

View File

@ -161,159 +161,6 @@ public static function evaluateGovernanceContract(array $contract, float $measur
], 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

View File

@ -14,10 +14,6 @@ final class TestLaneManifest
{
private const ARTIFACT_DIRECTORY = 'storage/logs/test-lanes';
private const MAINLINE_BRANCH = 'dev';
private const CI_RUNNER_LABEL = 'ubuntu-latest';
private const FULL_SUITE_BASELINE_SECONDS = 2625;
private const COMMAND_REFS = [
@ -56,9 +52,8 @@ final class TestLaneManifest
public static function manifest(): array
{
return [
'version' => 2,
'version' => 1,
'artifactDirectory' => self::artifactDirectory(),
'mainlineBranch' => self::mainlineBranch(),
'classifications' => self::classifications(),
'families' => self::families(),
'mixedFileResolutions' => self::mixedFileResolutions(),
@ -66,11 +61,6 @@ public static function manifest(): array
'driftGuards' => self::driftGuards(),
'budgetTargets' => self::budgetTargets(),
'lanes' => self::lanes(),
'workflowProfiles' => self::workflowProfiles(),
'laneBindings' => self::laneBindings(),
'budgetEnforcementProfiles' => TestLaneBudget::enforcementProfiles(),
'artifactPublicationContracts' => self::artifactPublicationContracts(),
'failureClasses' => self::failureClasses(),
'familyBudgets' => self::familyBudgets(),
'heavyGovernanceBudgetContract' => self::heavyGovernanceBudgetContract(),
'heavyGovernanceHotspotInventory' => self::heavyGovernanceHotspotInventory(),
@ -1891,349 +1881,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
{
if (! array_key_exists($laneId, self::COMMAND_REFS)) {
@ -2335,12 +1982,7 @@ public static function runLane(string $laneId): int
$capturedOutput .= $buffer;
});
TestLaneReport::finalizeLane(
$laneId,
microtime(true) - $startedAt,
$capturedOutput,
exitCode: $process->getExitCode() ?? 1,
);
TestLaneReport::finalizeLane($laneId, microtime(true) - $startedAt, $capturedOutput);
return $process->getExitCode() ?? 1;
}
@ -2371,7 +2013,6 @@ public static function renderLatestReport(string $laneId, ?string $comparisonPro
laneId: $laneId,
report: $report,
profileOutput: is_file($profileOutputPath) ? (string) file_get_contents($profileOutputPath) : null,
exitCode: 0,
);
echo json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR).PHP_EOL;
@ -2757,31 +2398,6 @@ private static function appRoot(): string
return dirname(__DIR__, 2);
}
public static function repoRoot(): string
{
$configuredRoot = getenv('TENANTATLAS_REPO_ROOT');
if (is_string($configuredRoot) && $configuredRoot !== '') {
if (str_starts_with($configuredRoot, DIRECTORY_SEPARATOR)) {
return rtrim($configuredRoot, DIRECTORY_SEPARATOR);
}
$resolvedConfiguredRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.$configuredRoot);
if ($resolvedConfiguredRoot !== false) {
return $resolvedConfiguredRoot;
}
}
$resolvedDefaultRoot = realpath(self::appRoot().DIRECTORY_SEPARATOR.'../..');
if ($resolvedDefaultRoot !== false) {
return $resolvedDefaultRoot;
}
return dirname(dirname(self::appRoot()));
}
/**
* @param list<string> $targets
*/

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>}
*/
@ -270,7 +89,6 @@ public static function buildReport(
array $durationsByFile,
?string $artifactDirectory = null,
?string $comparisonProfile = null,
?array $ciContext = null,
): array {
$lane = TestLaneManifest::lane($laneId);
$heavyGovernanceContract = $laneId === 'heavy-governance'
@ -284,17 +102,6 @@ public static function buildReport(
$laneBudget = TestLaneBudget::fromArray($lane['budget']);
$laneBudgetEvaluation = $laneBudget->evaluate($wallClockSeconds);
$resolvedCiContext = array_filter($ciContext ?? TestLaneManifest::currentCiContext($laneId), static fn (mixed $value): bool => $value !== null);
$ciBudgetEvaluation = null;
if (is_string($resolvedCiContext['triggerClass'] ?? null) && $resolvedCiContext['triggerClass'] !== '') {
$ciBudgetEvaluation = TestLaneBudget::evaluateLaneForTrigger(
$laneId,
(string) $resolvedCiContext['triggerClass'],
$wallClockSeconds,
);
}
$artifactPaths = self::artifactPaths($laneId, $artifactDirectory);
$artifacts = [];
@ -396,12 +203,6 @@ classificationAttribution: $attribution['classificationAttribution'],
static fn (array $evaluation): bool => ($evaluation['targetType'] ?? null) === 'family',
)),
'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 !== []) {
@ -418,14 +219,6 @@ classificationAttribution: $attribution['classificationAttribution'],
$report['sharedFixtureSlimmingComparison'] = $comparison;
}
if ($resolvedCiContext !== []) {
$report['ciContext'] = $resolvedCiContext;
}
if ($ciBudgetEvaluation !== null) {
$report['ciBudgetEvaluation'] = $ciBudgetEvaluation;
}
return $report;
}
@ -438,7 +231,6 @@ public static function writeArtifacts(
array $report,
?string $profileOutput = null,
?string $artifactDirectory = null,
?int $exitCode = 0,
): array {
$artifactPaths = self::artifactPaths($laneId, $artifactDirectory);
@ -449,45 +241,40 @@ public static function writeArtifacts(
self::buildSummaryMarkdown($report),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['budget']),
json_encode([
'laneId' => $report['laneId'],
'artifactDirectory' => $report['artifactDirectory'],
'wallClockSeconds' => $report['wallClockSeconds'],
'budgetThresholdSeconds' => $report['budgetThresholdSeconds'],
'budgetBaselineSource' => $report['budgetBaselineSource'],
'budgetEnforcement' => $report['budgetEnforcement'],
'budgetLifecycleState' => $report['budgetLifecycleState'],
'budgetStatus' => $report['budgetStatus'],
'classificationAttribution' => $report['classificationAttribution'],
'familyAttribution' => $report['familyAttribution'],
'budgetEvaluations' => $report['budgetEvaluations'],
'familyBudgetEvaluations' => $report['familyBudgetEvaluations'],
'budgetContract' => $report['budgetContract'] ?? null,
'inventoryCoverage' => $report['inventoryCoverage'] ?? null,
'budgetSnapshots' => $report['budgetSnapshots'] ?? null,
'budgetOutcome' => $report['budgetOutcome'] ?? null,
'remainingOpenFamilies' => $report['remainingOpenFamilies'] ?? null,
'stabilizedFamilies' => $report['stabilizedFamilies'] ?? null,
'sharedFixtureSlimmingComparison' => $report['sharedFixtureSlimmingComparison'] ?? null,
], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
file_put_contents(
TestLaneManifest::absolutePath($artifactPaths['report']),
json_encode($report, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR),
);
if (is_string($profileOutput) && trim($profileOutput) !== '') {
file_put_contents(TestLaneManifest::absolutePath($artifactPaths['profile']), $profileOutput);
}
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;
}
@ -499,21 +286,18 @@ public static function finalizeLane(
float $wallClockSeconds,
string $capturedOutput = '',
?string $comparisonProfile = null,
?int $exitCode = 0,
): array {
$artifactPaths = self::artifactPaths($laneId);
$parsed = self::parseJUnit(TestLaneManifest::absolutePath($artifactPaths['junit']), $laneId);
$ciContext = TestLaneManifest::currentCiContext($laneId);
$report = self::buildReport(
laneId: $laneId,
wallClockSeconds: $wallClockSeconds,
slowestEntries: $parsed['slowestEntries'],
durationsByFile: $parsed['durationsByFile'],
comparisonProfile: $comparisonProfile,
ciContext: $ciContext,
);
self::writeArtifacts($laneId, $report, $capturedOutput, exitCode: $exitCode);
self::writeArtifacts($laneId, $report, $capturedOutput);
return $report;
}
@ -532,18 +316,6 @@ private static function buildSummaryMarkdown(array $report): string
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)',
@ -903,76 +675,6 @@ static function (float $carry, array $entry) use ($coveredFamilyIds): float {
];
}
/**
* @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
{
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

@ -7,8 +7,6 @@ ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/platform"
LANE="${1:-fast-feedback}"
CAPTURE_BASELINE=false
WORKFLOW_ID=""
TRIGGER_CLASS=""
copy_heavy_baseline_artifacts() {
local artifact_root="${APP_DIR}/storage/logs/test-lanes"
@ -59,16 +57,6 @@ for arg in "$@"; do
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
@ -77,14 +65,6 @@ if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LAN
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}"
if [[ ${#remaining_args[@]} -gt 0 ]]; then

View File

@ -7,8 +7,6 @@ ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
APP_DIR="${ROOT_DIR}/apps/platform"
LANE="${1:-fast-feedback}"
CAPTURE_BASELINE=false
WORKFLOW_ID=""
TRIGGER_CLASS=""
copy_heavy_baseline_artifacts() {
local artifact_root="${APP_DIR}/storage/logs/test-lanes"
@ -54,16 +52,6 @@ for arg in "$@"; do
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
@ -73,18 +61,10 @@ if [[ "${CAPTURE_BASELINE}" == true && "${LANE}" != "heavy-governance" && "${LAN
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}"
./vendor/bin/sail composer run --timeout=0 "${COMPOSER_SCRIPT}"
if [[ "${CAPTURE_BASELINE}" == true ]]; then
copy_heavy_baseline_artifacts
fi
fi

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.