# Implementation Plan: Cross-Tenant Compare Preview and Promotion Preflight **Branch**: `043-cross-tenant-compare-and-promotion` | **Date**: 2026-04-27 | **Spec**: [spec.md](spec.md) **Input**: Feature specification from [spec.md](spec.md) ## Summary Refresh Spec 043 into a narrow, implementation-ready workflow that adds one canonical workspace-context compare page under `/admin`, one reusable compare preview builder, and one read-only promotion preflight action. The slice reuses existing baseline compare subject identity, portfolio-triage context continuity, capability resolvers, and workspace audit logging. It deliberately stops before actual promotion execution, queueing, or persisted promotion drafts. Filament remains on Livewire v4, no panel-provider registration changes are required (`bootstrap/providers.php` remains the authoritative provider registration location), no globally searchable compare resource is added, and no new panel asset bundle is expected. ## Technical Context **Language/Version**: PHP 8.4, Laravel 12 **Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing baseline compare services, portfolio-triage seams, audit services, and capability resolvers **Storage**: PostgreSQL via existing inventory, policy-version, and audit tables; no new compare or promotion table **Testing**: Pest v4 `Unit` and `Feature` coverage only **Validation Lanes**: fast-feedback, confidence **Target Platform**: Laravel monolith in `apps/platform`, admin panel only (`/admin`) **Project Type**: Web application (Laravel monolith with Filament pages) **Performance Goals**: compare preview and promotion preflight stay synchronous and derived from existing persisted truth; no background execution path in v1 **Constraints**: no target mutation, no `OperationRun`, no queue, no new persisted draft, no cross-workspace compare, no raw payload view by default **Scale/Scope**: 2 tenant selectors, 1 canonical compare page, 1 preflight action, 1 launch/return continuity path, focused reuse of existing compare builders ## UI / Surface Guardrail Plan - **Guardrail scope**: one new canonical compare page plus one launch action from existing tenant-registry/portfolio context - **Native vs custom classification summary**: native Filament page with shared compare/audit/navigation primitives - **Shared-family relevance**: canonical admin pages, compare drill-down patterns, launch actions, audit-backed modal/action copy - **State layers in scope**: page, query state - **Audience modes in scope**: operator-MSP only - **Decision/diagnostic/raw hierarchy plan**: decision-first compare summary, diagnostics second, raw evidence stays on existing tenant/baseline surfaces - **Raw/support gating plan**: no new raw/support surface; keep payload proof behind existing pages - **One-primary-action / duplicate-truth control**: the compare page keeps one dominant next action, `Generate promotion preflight`; drill-down and return actions stay secondary - **Launch default**: the tenant-registry launch action prefills the launched tenant as `target tenant`; the operator chooses the source tenant explicitly - **Handling modes by drift class or surface**: review-mandatory; any actual promotion execution or queue path is exception-required and out of scope - **Repository-signal treatment**: review-mandatory - **Special surface test profiles**: standard-native-filament - **Required tests or manual smoke**: functional-core, state-contract - **Exception path and spread control**: none - **Active feature PR close-out entry**: Guardrail ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes - **Systems touched**: - `App\Filament\Pages\BaselineCompareLanding` - `App\Filament\Pages\BaselineCompareMatrix` - `App\Filament\Resources\TenantResource` - `App\Filament\Resources\TenantResource\Pages\ListTenants` - `App\Services\Baselines\BaselineCompareService` - `App\Support\Baselines\BaselineCompareMatrixBuilder` - `App\Support\Baselines\Compare\CompareStrategyRegistry` - `App\Services\PortfolioTriage\TenantTriageReviewService` - `App\Services\Audit\WorkspaceAuditLogger` - `App\Support\Audit\AuditActionId` - `App\Support\Navigation\CanonicalNavigationContext` - **Shared abstractions reused**: capability resolvers, baseline compare strategy selection, canonical navigation context, existing audit recorder/logger path, and tenant-registry return-state conventions - **New abstraction introduced? why?**: one narrow compare preview builder and one narrow promotion preflight service, because no existing service accepts source+target tenant scope and computes promotion readiness without execution - **Why the existing abstraction was sufficient or insufficient**: tenant-level baseline compare is sufficient for subject identity, evidence posture, and drill-down semantics, but insufficient for dual-tenant scope and promotion-readiness reasoning - **Bounded deviation / spread control**: no local compare sidecars on tenant pages; future callers must route through the canonical compare page and its services ## OperationRun UX Impact - **Touches OperationRun start/completion/link UX?**: no - **Central contract reused**: `N/A` - **Delegated UX behaviors**: `N/A` - **Surface-owned behavior kept local**: compare preview and preflight remain synchronous and read-only - **Queued DB-notification policy**: `N/A` - **Terminal notification path**: `N/A` - **Exception path**: none ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: yes - **Provider-owned seams**: Microsoft-first inventory subject identity and policy-type mapping remain inside existing baseline compare strategy selection and inventory data - **Platform-core seams**: source/target tenant scope, compare preview contract, promotion preflight contract, operator-facing readiness vocabulary - **Neutral platform terms / contracts preserved**: `source tenant`, `target tenant`, `governed subject`, `compare preview`, `promotion preflight`, and `blocked reason` - **Retained provider-specific semantics and why**: existing policy-type and inventory semantics remain Microsoft-first because this repo still has one real provider domain; the compare page should not invent fake provider-neutral mapping logic above that seam - **Bounded extraction or follow-up path**: follow-up-spec only if later provider domains become current-release truth ## Constitution Check *GATE: Must pass before implementation preparation continues.* - Inventory-first: PASS. Compare preview and preflight derive from existing inventory and policy-version truth rather than a new compare snapshot. - Read/write separation: PASS. This slice stays read-only; no write execution is introduced. - Graph contract path: PASS. No new Graph endpoint or direct provider call is added. - Deterministic capabilities: PASS. Reuse existing capability registries such as `Capabilities::TENANT_VIEW`, `Capabilities::WORKSPACE_BASELINES_VIEW`, `Capabilities::WORKSPACE_BASELINES_MANAGE`, and existing tenant sync/manage seams. - Workspace and tenant isolation: PASS. The compare page must resolve workspace membership first and source/target entitlement second, with `404` for inaccessible tenants. - RBAC-UX plane separation: PASS. This slice lives only in `/admin`; no `/system` or cross-plane route is introduced. - Destructive action discipline: PASS by non-use. The slice contains no destructive action. - Global search: PASS. No new Resource or Global Search result is introduced. - OperationRun / Ops-UX: PASS by non-use. Actual promotion execution is deferred. - Data minimization: PASS. The compare page summarizes derived readiness and blocks; raw payloads stay on existing tenant/baseline pages. - Test governance: PASS. Proof stays in `Unit` plus `Feature`; no browser or heavy-governance expansion is planned. - Proportionality / no premature abstraction: PASS. One preview builder and one preflight service are justified by the dual-tenant workflow; no new persistence or framework layer is added. - Persisted truth: PASS. No new compare or promotion table. - Behavioral state: PASS. Readiness and blocked reasons remain derived, not persisted. - Shared pattern first / UI semantics / Filament-native UI: PASS. Existing compare, navigation, and audit paths are extended rather than replaced. - Provider boundary: PASS. Microsoft-shaped subject matching stays in existing strategy seams; the page contract stays platform-neutral. - Filament/Laravel panel safety: PASS. Filament v5 remains on Livewire v4, no provider registration change beyond `bootstrap/providers.php`, and no new assets are planned. **Gate evaluation**: PASS. ## Test Governance Check - **Test purpose / classification by changed surface**: `Feature` for the compare page, launch context, auth, and audit; `Unit` for compare preview matching and promotion-preflight classification - **Affected validation lanes**: fast-feedback, confidence - **Why this lane mix is the narrowest sufficient proof**: feature tests prove the Filament page and launch path while unit tests keep preview/preflight rules cheap and isolated. Browser or heavy-governance coverage is not required for the first read-only slice. - **Narrowest proving command(s)**: - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/PortfolioCompare/CrossTenantComparePreviewBuilderTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Support/PortfolioCompare/CrossTenantPromotionPreflightTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PortfolioCompare/CrossTenantComparePageTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PortfolioCompare/CrossTenantCompareAuthorizationTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PortfolioCompare/CrossTenantPromotionPreflightAuditTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php` - **Fixture / helper / factory / seed / context cost risks**: reuse existing inventory, baseline compare, tenant registry, and portfolio-triage fixtures; avoid browser setup, queue fixtures, or seeded promotion history - **Expensive defaults or shared helper growth introduced?**: no - **Heavy-family additions, promotions, or visibility changes**: none - **Surface-class relief / special coverage rule**: standard-native-filament - **Closing validation and reviewer handoff**: rerun the six focused commands above and confirm the slice remains read-only, deny-as-not-found-safe, and grounded on existing compare + portfolio seams - **Budget / baseline / trend follow-up**: none expected - **Review-stop questions**: lane fit, hidden fixture growth, accidental write execution, accidental queue/runtime scope - **Escalation path**: `document-in-feature` for contained lane drift, `reject-or-split` for any attempt to add execution scope - **Active feature PR close-out entry**: Guardrail - **Why no dedicated follow-up spec is needed**: test upkeep remains feature-local; only actual promotion execution or multi-provider compare would warrant a separate follow-up spec ## Project Structure ### Documentation (this feature) ```text specs/043-cross-tenant-compare-and-promotion/ ├── checklists/ │ └── requirements.md ├── spec.md ├── plan.md └── tasks.md ``` This refresh intentionally limits itself to the core preparation package plus `checklists/requirements.md`. No additional research/data-model/contracts artifact is required to make the narrowed slice implementation-ready. ### Source Code (repository root) ```text apps/platform/ ├── app/ │ ├── Filament/Pages/ │ │ ├── BaselineCompareLanding.php │ │ ├── BaselineCompareMatrix.php │ │ └── [new canonical compare page] │ ├── Filament/Resources/TenantResource.php │ ├── Filament/Resources/TenantResource/Pages/ListTenants.php │ ├── Models/ │ │ ├── InventoryItem.php │ │ └── PolicyVersion.php │ ├── Services/Audit/ │ │ └── WorkspaceAuditLogger.php │ ├── Services/Baselines/ │ │ └── BaselineCompareService.php │ ├── Services/PortfolioTriage/ │ │ └── TenantTriageReviewService.php │ ├── Support/Audit/AuditActionId.php │ ├── Support/Baselines/ │ │ ├── BaselineCompareMatrixBuilder.php │ │ └── Compare/CompareStrategyRegistry.php │ └── Support/PortfolioCompare/ or Services/PortfolioCompare/ └── tests/ ├── Feature/PortfolioCompare/ └── Unit/Support/PortfolioCompare/ ``` **Structure Decision**: keep implementation inside `apps/platform`, reuse existing compare and portfolio seams, and introduce at most one small `PortfolioCompare` support/service namespace for the new dual-tenant preview/preflight logic. ## Complexity Tracking | Violation | Why Needed | Simpler Alternative Rejected Because | |-----------|------------|-------------------------------------| | New compare preview builder | dual-tenant compare needs one place to translate existing inventory/baseline truth into a canonical preview contract | page-local mapping would duplicate compare logic and drift from existing baseline compare seams | | New promotion preflight service | readiness reasoning must stay read-only and auditable before any execution path exists | bolting readiness rules into the page would make later reuse and testing brittle | ## Proportionality Review - **Current operator problem**: portfolio operators still lack one bounded surface that answers whether a target tenant can follow a source tenant. - **Existing structure is insufficient because**: existing baseline compare is tenant-vs-reference, not tenant-vs-tenant, and portfolio triage does not compute promotion readiness. - **Narrowest correct implementation**: one canonical page plus one preview builder and one preflight service, no new table, no execution path. - **Ownership cost created**: maintain a small preview/preflight contract and a focused test family. - **Alternative intentionally rejected**: actual promotion execution, persisted promotion drafts, and local compare sidecars were rejected as premature. - **Release truth**: current-release gap, not speculative platform work. ## Implementation Strategy ### Suggested MVP Scope MVP = **US1 + US2 together**. A compare page without a promotion preflight leaves the core decision incomplete, and a preflight without a canonical compare page has no trustworthy operator context. ### Incremental Delivery 1. Reuse current compare, navigation, capability, and audit seams. 2. Deliver the canonical compare preview. 3. Add the read-only promotion preflight on top of the same page and services. 4. Add launch/return continuity from portfolio-triage and tenant-registry context. 5. Finish with narrow validation and formatting. ### Team Strategy 1. Settle the preview/preflight contracts first. 2. Parallelize unit tests for preview/preflight rules and feature tests for page/auth behavior. 3. Serialize merges around the canonical compare page and the shared `PortfolioCompare` service namespace so the page contract does not drift.