# Implementation Plan: Cross-Tenant Promotion Execution v1 **Branch**: `264-cross-tenant-promotion-execution` | **Date**: 2026-05-02 | **Spec**: [spec.md](./spec.md) **Input**: Feature specification from `/specs/264-cross-tenant-promotion-execution/spec.md` ## Summary This plan is the execution delta over Spec 043 and the current cross-tenant compare code path. The existing compare page, compare preview builder, promotion preflight, launch context, and preflight audit are inherited. The implementation scope is only to add one bounded `Execute promotion` path that reuses the current compare and preflight truth, requires explicit confirmation, queues exactly one canonical `promotion.execute` `OperationRun`, and keeps result truth on the shared Monitoring path. The implementation must not add a persisted promotion-draft or compare-snapshot entity. The most important repo-truth constraint is already visible in current code: `RestoreService::executeFromPolicyVersion()` rejects foreign-tenant `PolicyVersion` records, so the execution path cannot be implemented as a naive direct call to the existing policy-version restore job. v1 therefore needs one bounded promotion execution planner or bridge that translates source-tenant content into target-safe write inputs while still delegating the actual target mutation through current provider-write seams. ## Inherited Baseline / Explicit Delta ### Inherited baseline - `CrossTenantComparePage` already owns the canonical `/admin/cross-tenant-compare` selection and preflight surface. - `CrossTenantCompareSelection`, `CrossTenantComparePreviewBuilder`, and `CrossTenantPromotionPreflight` already provide reproducible compare and readiness truth. - tenant-registry and portfolio launch context plus return-state continuity already work. - preflight audit already uses the current workspace audit pipeline. - current tests already prove compare preview, preflight, authorization, audit, and launch continuity. ### Explicit delta in this plan - add one queued `promotion.execute` run type and the smallest supporting control, capability, and audit wiring required for it - add one bounded promotion execution planner or bridge that consumes the current compare and preflight truth - wire `Execute promotion` onto the current compare page with explicit confirmation and shared start-result UX - keep Monitoring continuity on the existing `OperationRun` viewer and current run-link contract ## Technical Context **Language/Version**: PHP 8.4, Laravel 12, Filament v5, Livewire v4 **Primary Dependencies**: `CrossTenantComparePage`, `CrossTenantComparePreviewBuilder`, `CrossTenantPromotionPreflight`, `OperationRunService`, `OperationCatalog`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, `WorkspaceAuditLogger`, `AuditActionId`, and the current policy-version or restore write seam **Storage**: PostgreSQL tables already in use for `OperationRun`, audit logs, policy versions, inventory, and existing provider-write artifacts **Testing**: Pest unit and feature tests plus one bounded browser smoke **Validation Lanes**: `fast-feedback`, `confidence`, `browser` **Target Platform**: existing Laravel admin runtime under `apps/platform` **Project Type**: Laravel monolith with Filament admin surfaces **Performance Goals**: no synchronous provider mutation from the compare page, no second queue family, and no new heavy browser or provider fixture domain **Constraints**: no promotion-draft table, no compare-snapshot table, no direct foreign-tenant `PolicyVersion` execution, no multi-target batch, no approval chain, no rollback engine **Scale/Scope**: one source tenant, one target tenant, one current compare scope, one queued run ## UI / Surface Guardrail Plan - **Guardrail scope**: changed surfaces - **Native vs custom classification summary**: native Filament compare page plus shared `OperationRun` UX only - **Shared-family relevance**: compare-page header actions, confirmation modal, queued start feedback, run links, Monitoring continuity - **State layers in scope**: page state, query state, preflight state, action state, confirmation modal state, and existing run-link state - **Audience modes in scope**: operator-MSP only - **Decision/diagnostic/raw hierarchy plan**: decision-first compare summary and mutation scope first, subject-level execution diagnostics second, raw provider detail last and kept off the compare page - **Raw/support gating plan**: raw provider payloads, provider IDs, and worker detail remain behind existing tenant or Monitoring detail surfaces - **One-primary-action / duplicate-truth control**: the compare page keeps exactly one dominant next action at a time, and Monitoring remains the only progress detail surface after queueing - **Handling modes by drift class or surface**: `review-mandatory` for any drift toward a second promotion surface, draft persistence, or local notification flow - **Repository-signal treatment**: `review-mandatory` - **Special surface test profiles**: `standard-native-filament` - **Required tests or manual smoke**: focused PortfolioCompare feature coverage plus one bounded browser smoke for compare-to-operation handoff - **Exception path and spread control**: none; any proposal for draft persistence, approvals, rollback, or batch execution is a scope split, not an in-feature exception - **Active feature PR close-out entry**: Smoke Coverage ## Shared Pattern & System Fit - **Cross-cutting feature marker**: yes - **Systems touched**: `CrossTenantComparePage`, compare preview and preflight services, operation catalog and capability wiring, current Monitoring links, audit logging, and the bounded provider-write bridge for target mutation - **Shared abstractions reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, `WorkspaceUiEnforcement`, `WorkspaceAuditLogger`, and current compare and preflight services - **New abstraction introduced? why?**: yes. One bounded `PortfolioCompare` execution planner or bridge is required because current restore helpers are tenant-owned and reject foreign-tenant policy versions. One queued promotion job is required because target mutation must not run synchronously on the compare page. - **Why the existing abstraction was sufficient or insufficient**: the current compare and preflight seam is sufficient for readiness and exclusion truth. It is insufficient for execution because it does not generate a target-safe mutation plan or a queued run identity. - **Bounded deviation / spread control**: keep the new logic local to the `PortfolioCompare` domain and current run UX seams. Do not create a new promotion module, workspace, or dashboard family. ## OperationRun UX Impact - **Touches OperationRun start/completion/link UX?**: yes - **Central contract reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, and `OpsUxBrowserEvents` - **Delegated UX behaviors**: queued or deduped start messaging, blocked or scope-busy messaging, Monitoring link generation, run-enqueued browser event, and terminal notification stay on the shared run UX layer - **Surface-owned behavior kept local**: compare-page confirmation copy, excluded-subject explanation, and current launch or return-state preservation remain local to the compare page - **Queued DB-notification policy**: no new queued-only DB notification path - **Terminal notification path**: existing `OperationRun` completion path remains authoritative - **Exception path**: none ## Provider Boundary & Portability Fit - **Shared provider/platform boundary touched?**: yes - **Provider-owned seams**: current policy-version capture, restore semantics, assignment or scope-tag handling, and provider write execution stay provider-owned - **Platform-core seams**: compare-page wording, run vocabulary, authorization, operational control labels, and Monitoring continuity stay platform-owned - **Neutral platform terms / contracts preserved**: source tenant, target tenant, governed subject, promotion execution, ready subject, blocked reason, manual mapping required - **Retained provider-specific semantics and why**: Microsoft-specific payload and target mutation semantics remain inside the existing provider-write seam because the repo currently has one real provider - **Bounded extraction or follow-up path**: if current provider-write seams cannot support a bounded bridge without widening persistence, stop and split before introducing a second promotion truth ## Constitution Check *GATE: Must pass before implementation begins and again before merge.* - Inventory-first: compare preview and preflight remain derived from current tenant-owned inventory or captured content; execution consumes that truth instead of inventing a parallel catalog - Read/write separation: preflight stays read-only; mutation begins only after explicit confirmation and queued execution - Graph contract path: direct Graph or provider writes must stay behind the current provider-write seam, never in the Filament page action itself - Deterministic capabilities: execution stays on existing capability registries and current workspace or tenant isolation rules - RBAC-UX: inaccessible tenants remain `404`; execution denials remain explicit `403` only for in-scope actors forcing a blocked mutation path - Workspace isolation: unchanged - Tenant isolation: source tenant stays read-only; target tenant owns the mutation boundary - Run observability: exactly one canonical `promotion.execute` `OperationRun` is required - OperationRun start UX: shared start UX remains authoritative - Ops-UX lifecycle and summary counts: stay on existing `OperationSummaryKeys`; no new summary-key family - Test governance: keep proof bounded to PortfolioCompare unit, feature, and one browser smoke file only - Proportionality / persistence / bloat: no new table, no approval chain, no rollback, no draft persistence - Shared pattern first: current compare page and run UX must be extended, not bypassed - Provider boundary: use the bounded bridge only to cross the tenant-owned restore restriction; do not generalize it into a second provider abstraction layer - V1 explicitness / few layers: prefer one planner or bridge plus one job over a subsystem of drafts, approvals, and orchestration records - Filament-native UI: keep the current compare page as the only operator surface; no new panel or Laravel or Filament service-provider registration work is needed - UI or UX surface taxonomy and decision-first operating model: compare remains the decision surface and Monitoring remains the run viewer - Audience-aware disclosure: operators see readiness, mutation scope, and run link first; raw provider detail stays hidden by default - Action-surface discipline: the compare page keeps one dominant next action at a time ## Test Governance Check - **Test purpose / classification by changed surface**: Unit, Feature, Browser - **Affected validation lanes**: `fast-feedback`, `confidence`, `browser` - **Why this lane mix is the narrowest sufficient proof**: unit coverage proves plan derivation and ready-only filtering, feature coverage proves Filament action, auth, and audit behavior, and one browser smoke proves the confirmation modal plus Monitoring handoff on the real compare page - **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/CrossTenantPromotionExecutionPlannerTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionActionTest.php tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionAuthorizationTest.php tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionAuditTest.php tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionRunUxTest.php tests/Feature/PortfolioCompare/CrossTenantComparePageTest.php tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/PortfolioCompare/CrossTenantPromotionExecutionSmokeTest.php` - `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - **Fixture / helper / factory / seed / context cost risks**: reuse current portfolio-compare fixtures and current `OperationRun` assertions; avoid new provider seed domains or broad Monitoring fixtures - **Expensive defaults or shared helper growth introduced?**: no - **Heavy-family additions, promotions, or visibility changes**: one new bounded browser smoke file only - **Surface-class relief / special coverage rule**: `standard-native-filament` with required real-browser confirmation coverage - **Closing validation and reviewer handoff**: reviewers should confirm that the queued path, audit trail, and Monitoring handoff all stay on shared seams with no draft persistence - **Budget / baseline / trend follow-up**: none - **Review-stop questions**: lane fit, no-draft persistence, run type or control naming, and bounded target-write bridge only - **Escalation path**: none - **Active feature PR close-out entry**: Smoke Coverage ## Project Structure ### Documentation (this feature) ```text specs/264-cross-tenant-promotion-execution/ ├── spec.md ├── plan.md ├── tasks.md └── checklists/ └── requirements.md ``` ### Source Code (expected implementation surfaces) ```text apps/platform/app/Filament/Pages/CrossTenantComparePage.php apps/platform/app/Support/PortfolioCompare/CrossTenantComparePreviewBuilder.php apps/platform/app/Support/PortfolioCompare/CrossTenantPromotionPreflight.php apps/platform/app/Support/PortfolioCompare/CrossTenantPromotionExecutionPlanner.php apps/platform/app/Services/PortfolioCompare/CrossTenantPromotionExecutionService.php apps/platform/app/Jobs/Operations/CrossTenantPromotionExecutionJob.php apps/platform/app/Support/OperationCatalog.php apps/platform/app/Support/OperationRunType.php apps/platform/app/Support/Operations/OperationRunCapabilityResolver.php apps/platform/app/Support/OperationalControls/OperationalControlCatalog.php apps/platform/app/Services/Providers/ProviderOperationRegistry.php apps/platform/app/Support/Audit/AuditActionId.php apps/platform/app/Services/Audit/WorkspaceAuditLogger.php apps/platform/app/Support/OperationRunLinks.php apps/platform/app/Support/Navigation/RelatedNavigationResolver.php apps/platform/app/Services/OperationRunService.php apps/platform/app/Services/Intune/RestoreService.php apps/platform/tests/Unit/Support/PortfolioCompare/CrossTenantPromotionExecutionPlannerTest.php apps/platform/tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionActionTest.php apps/platform/tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionAuthorizationTest.php apps/platform/tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionAuditTest.php apps/platform/tests/Feature/PortfolioCompare/CrossTenantPromotionExecutionRunUxTest.php apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php apps/platform/tests/Browser/PortfolioCompare/CrossTenantPromotionExecutionSmokeTest.php ``` **Structure Decision**: keep the implementation inside the current compare page, `PortfolioCompare` support or service layer, and current `OperationRun` UX seams. Add at most one bounded planner or bridge, one execution service, and one queued job. ## Data / Migration Implications - Prefer existing `OperationRun.context`, `OperationRun.summary_counts`, and audit metadata over new persistence. - No new table or migration is expected for v1. - If operation or control naming needs only PHP registry changes, keep it there and avoid schema work. - If implementation cannot bridge source content into target-safe mutation inputs without a new persisted promotion-draft or snapshot entity, stop and split the feature rather than widening the current package. ## Rollout Considerations - Filament remains v5 on Livewire v4. No new Laravel or Filament service-provider registration change is required, and service-provider registration remains in `apps/platform/bootstrap/providers.php`. - No global search change is required because the affected surface is an existing page and the Monitoring viewer is already in place. - No new asset registration is expected. - The new mutating action is not destructive in the delete sense, but it must still use explicit confirmation on the compare page. - Existing queue workers remain the deployment requirement for the new run path. ## Risk Controls - Reject any implementation that persists promotion drafts, compare snapshots, or approval records. - Reject any implementation that reuses `RestoreService::executeFromPolicyVersion()` by pretending the source version belongs to the target tenant. - Reject any implementation that introduces a second promotion surface, a second queue domain, or a promotion-specific dashboard. - Reject any implementation that invents new run-summary keys instead of staying on canonical keys. - Keep blocked and manual-mapping-required subjects excluded from target mutation in all paths. ## Implementation Phases ### Phase 0 - Confirm the bounded execution seam - Reconfirm the current compare and preflight truth plus the current restore restriction that foreign-tenant `PolicyVersion` execution is not directly allowed. - Decide the smallest target-safe bridge from source content to target write inputs without adding persistence. ### Phase 1 - Add operation vocabulary and bounded execution planning - Add one canonical `promotion.execute` operation type, its control key, run capability mapping, any `ProviderOperationRegistry` entry only if the chosen shared start-result seam requires it, and audit action IDs. - Add one bounded planner or bridge that derives ready-only execution inputs and stable run identity values from the current compare and preflight truth. `ProviderOperationRegistry` remains application operation-vocabulary wiring only. It is not Laravel or Filament service-provider registration. ### Phase 2 - Wire the compare page action and shared start UX - Add `Execute promotion` to the current compare page with explicit confirmation, single-primary-action discipline, and preserved source, target, and return-state context. - Reuse shared start-result UX for queued, deduped, blocked, and Monitoring-link behaviors. ### Phase 3 - Execute the target mutation through one queued run - Add one queued promotion execution job that consumes `OperationRun.context`, delegates actual writes through current provider-write seams, and records summary counts using canonical keys only. - Keep the compare page free of synchronous target mutation. ### Phase 4 - Audit, Monitoring continuity, and stop - Record start and terminal audit metadata for promotion execution. - Ensure Monitoring links, related navigation, and labels remain canonical for the new run type. - Run the planned validation commands and stop without widening into drafts, approval flow, rollback, or batch execution. ## Why This Plan Is Narrow Enough The repo already has the compare page, preflight truth, audit path, queue infrastructure, and Monitoring viewer. This plan adds only the missing bounded execution seam: one confirmed page action, one run type, one ready-only execution planner or bridge, and one queued worker. Everything larger stays explicitly deferred.