TenantAtlas/specs/264-cross-tenant-promotion-execution/spec.md
Ahmed Darrazi 983abb18a1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m22s
chore: commit workspace changes (automated)
2026-05-02 16:36:21 +02:00

311 lines
34 KiB
Markdown

# Feature Specification: Cross-Tenant Promotion Execution v1
**Feature Branch**: `264-cross-tenant-promotion-execution`
**Created**: 2026-05-02
**Status**: Ready for implementation
**Input**: User description: "Cross-Tenant Promotion Execution v1"
## Inherited Baseline / Explicit Delta
This package is an explicit delta follow-up over Spec 043 and the current cross-tenant compare code path.
### Inherited baseline
- `CrossTenantComparePage` already owns the canonical `/admin/cross-tenant-compare` decision surface.
- `CrossTenantCompareSelection`, `CrossTenantComparePreviewBuilder`, and `CrossTenantPromotionPreflight` already produce reproducible read-only compare and readiness truth.
- tenant-registry launch context, exact-two compare launch, and return-state preservation already exist.
- preflight audit already lands on the existing workspace audit pipeline through `AuditActionId::CrossTenantPromotionPreflightGenerated`.
- the current slice is intentionally read-only and explicitly deferred actual promotion execution, queueing, `OperationRun`, persisted compare snapshots, persisted promotion drafts, mapping automation, and customer-facing compare.
### Explicit delta in this spec
- add one bounded `Execute promotion` action from the current compare and preflight context
- require explicit confirmation before any target mutation starts
- queue exactly one canonical `OperationRun` for promotion execution and reuse the shared start/result UX
- mutate the target tenant for `ready` subjects only while keeping `blocked` and `manual_mapping_required` subjects excluded and visible
- keep execution truth on existing `OperationRun` and audit records instead of adding a new promotion-draft or compare-snapshot table
Everything broader remains inherited or explicitly out of scope.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: The product can already compare tenants and generate a read-only promotion preflight, but operators still cannot execute the bounded promotion step that the page recommends.
- **Today's failure**: Cross-tenant compare ends at advice. Operators still need manual, off-platform steps to apply source settings to the target tenant and then separately reconstruct whether the action actually ran.
- **User-visible improvement**: An authorized workspace operator can review a current compare preflight, confirm the scope, queue one cross-tenant promotion run, and follow that run through the existing Monitoring flow without leaving the canonical compare page.
- **Smallest enterprise-capable version**: One compare-page execution action, one confirmation modal, one queued `OperationRun`, one bounded promotion execution planner and worker, one truthful Monitoring handoff, and explicit audit metadata. No draft persistence, no batch execution, and no rollback workflow ship in v1.
- **Explicit non-goals**: No persisted promotion draft entity, no scheduled or recurring promotion, no multi-target batch execution, no approval workflow, no rollback engine, no customer-facing promotion, no cross-workspace execution, and no multi-provider abstraction expansion.
- **Permanent complexity imported**: One bounded execution planner or bridge, one queued promotion job, one new canonical operation type plus control key, a small audit extension, and focused unit, feature, and browser proof.
- **Why now**: `docs/product/spec-candidates.md`, `docs/product/roadmap.md`, and `docs/product/implementation-ledger.md` all still identify execution as the missing follow-up after the already-implemented read-only compare and preflight slice.
- **Why not local**: A page-local mutation button without a shared run contract would bypass the current compare truth, safety gates, audit seams, and Monitoring continuity.
- **Approval class**: Core Enterprise
- **Red flags triggered**: New mutating action, new queued run type, new write bridge over an existing read-only workflow. Defense: the slice stays on one compare page, one target tenant, one run type, zero new tables, and existing provider write seams.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 1 | Komplexitaet: 1 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: canonical-view
- **Primary Routes**:
- existing canonical `/admin/cross-tenant-compare` page for selection, preflight review, and execution confirmation
- existing `/admin/tenants` registry or portfolio-triage launch surfaces as the compare entrypoint and return context
- existing Monitoring operation-run detail flow as the canonical follow-up after a promotion run is queued
- **Data Ownership**:
- source-of-truth remains the current compare preview, preflight result, source-tenant `PolicyVersion` or equivalent captured content, and target-tenant inventory or restore truth
- runtime truth remains on `OperationRun`, current audit logs, and existing provider-write artifacts; no `CrossTenantPromotionDraft`, compare snapshot, or promotion-plan table is introduced
- any execution context must live in `OperationRun.context`, `OperationRun.summary_counts`, and audit metadata only
- **RBAC**:
- workspace and tenant scoping stay deny-as-not-found first for out-of-scope actors or tenants
- compare viewing keeps the existing 043 requirements: workspace baselines view plus tenant view on both source and target
- execution requires the compare-view permissions plus `Capabilities::WORKSPACE_BASELINES_MANAGE` on the workspace and `Capabilities::TENANT_MANAGE` on the target tenant
- source-tenant access stays read-only (`Capabilities::TENANT_VIEW`); the mutation boundary is the target tenant only
- members who can view compare but cannot execute still see the execution affordance only where the page already stays decision-oriented, and that affordance must be disabled with explicit permission guidance while forced execution attempts return `403`
- no new promotion-specific capability family may be introduced in v1
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: registry and portfolio launches continue to prefill the launched tenant as the `target tenant`, preserve the return-state token, and keep the `source tenant` intentionally chosen by the operator unless the current exact-two launch path already supplies both tenants.
- **Explicit entitlement checks preventing cross-tenant leakage**: the page must re-resolve workspace membership, source tenant scope, target tenant scope, and execution capability before preflight or execution logic runs. Any inaccessible tenant input is treated as not found, and no `OperationRun` may be created from inaccessible or stale selection input.
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
- **Cross-cutting feature?**: yes
- **Interaction class(es)**: compare-page header actions, confirmation modal copy, queued start notifications, Monitoring links, run status messaging, audit metadata, and return-state continuity
- **Systems touched**: `CrossTenantComparePage`, tenant-registry launch context, `CrossTenantCompareSelection`, `CrossTenantComparePreviewBuilder`, `CrossTenantPromotionPreflight`, `OperationRunService`, `OperationCatalog`, `OperationRunType`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, `OpsUxBrowserEvents`, `WorkspaceAuditLogger`, `AuditActionId`, `OperationalControlCatalog`, and the current policy-version or restore write seam
- **Existing pattern(s) to extend**: canonical compare page, current portfolio launch and return-state contract, shared `OperationRun` start UX, and existing Monitoring deep-link semantics
- **Shared contract / presenter / builder / renderer to reuse**: `CanonicalNavigationContext`, `WorkspaceUiEnforcement`, `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, `OpsUxBrowserEvents`, and the existing compare preview and preflight builders
- **Operation-registry distinction**: any `ProviderOperationRegistry` update in v1 is app-level operation-vocabulary wiring only, and only if the chosen shared start-result seam requires it. It is not Laravel or Filament service-provider registration.
- **Why the existing shared path is sufficient or insufficient**: compare and preflight already solve the selection and blocked-reason truth. They are insufficient for v1 execution because they do not produce a queued run, start/result UX, or target mutation plan. The current restore and policy-version seams solve provider write behavior and run lifecycle, but they are tenant-owned and cannot be pointed at a foreign tenant without a bounded promotion bridge.
- **Allowed deviation and why**: none. The feature must extend the current compare page and shared run UX instead of creating a second promotion console or a promotion-specific dashboard.
- **Consistency impact**: the terms `source tenant`, `target tenant`, `promotion preflight`, `Execute promotion`, `ready`, `blocked`, `manual mapping`, and `Open operation` must stay consistent across compare copy, confirmation copy, audit summaries, notifications, and Monitoring labels.
- **Review focus**: reviewers must block any persisted draft entity, direct Graph write from the page action, or local notification/run-link flow that bypasses the shared `OperationRun` UX contract.
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
- **Touches OperationRun start/completion/link UX?**: yes
- **Shared OperationRun UX contract/layer reused**: `OperationRunService`, `OperationRunLinks`, `OperationUxPresenter`, `ProviderOperationStartResultPresenter`, and `OpsUxBrowserEvents`
- **Delegated start/completion UX behaviors**: queued toast, deduped-or-already-running messaging, blocked or scope-busy result messaging, canonical Monitoring link generation, run-enqueued browser event dispatch, and normal terminal notification handling stay delegated to the shared run UX layer
- **Local surface-owned behavior that remains**: compare selection state, preflight summary, excluded-subject explanation, and confirmation-modal wording remain owned by `CrossTenantComparePage`
- **Queued DB-notification policy**: no new queued-only database-notification policy is introduced; v1 relies on the compare-page start result plus the existing terminal notification path
- **Terminal notification path**: keep the existing initiator-aware `OperationRun` completion path
- **Exception required?**: none
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
- **Shared provider/platform boundary touched?**: yes
- **Boundary classification**: mixed
- **Seams affected**: compare selection to execution planning, source content resolution, target mutation bridge, operation vocabulary, and provider-backed write delegation
- **Neutral platform terms preserved or introduced**: `source tenant`, `target tenant`, `governed subject`, `promotion execution`, `ready subject`, `blocked reason`, and `manual mapping required`
- **Provider-specific semantics retained and why**: Microsoft-first policy types, assignment semantics, scope tags, and provider payload translation remain inside the current policy-version, restore, and provider write seams because the repo currently has one real provider domain
- **Why this does not deepen provider coupling accidentally**: the page contract stays anchored on compare and preflight truth, and the queued mutation delegates into existing provider-write paths instead of exposing Graph-specific inputs or raw payload editing in the page surface
- **Follow-up path**: multi-provider promotion remains a separate follow-up if it ever becomes current-release truth
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|---|---|---|---|---|---|---|
| Canonical cross-tenant compare page | yes | Native Filament page plus shared compare and ops-ux primitives | compare summary, confirmation modal, run-start UX, Monitoring link continuity | page, query state, preflight state, action state, confirmation modal state | no | Reuses the current compare page; no second promotion surface is allowed |
| Tenant registry / portfolio launch action | no | N/A | current launch context only | none beyond existing deep-link state | no | Launch behavior remains inherited from Spec 043 |
| Monitoring operation-run detail | no | N/A | existing shared operation-run viewer | none beyond normal new-type rendering | no | v1 reuses the existing Monitoring viewer rather than introducing a promotion-specific detail screen |
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|---|---|---|---|---|---|---|---|
| Canonical cross-tenant compare page | Primary Decision Surface | Operator decides whether the ready portion of the current source selection should mutate the target tenant now | source and target summary, ready or blocked counts, exclusion counts, mutation scope, and the single next action | subject-level execution plan, mapping gaps, target exclusions, source and target drill-down links, and current run link after queueing | Primary because the compare page already owns the trusted preflight truth and can keep the execution decision tied to that truth | Moves directly from compare to confirmation to queued Monitoring handoff without a second workspace | Replaces off-platform handoff and manual note-taking with one bounded action path |
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|---|---|---|---|---|---|---|---|
| Canonical cross-tenant compare page | operator-MSP | source and target summary, preflight counts, excluded-subject totals, mutation scope, and current run link if a run exists | subject-level ready plan, mapping gaps, evidence freshness, and target-safe exclusions | raw provider payloads and deep provider diagnostics stay behind existing tenant, inventory, or Monitoring detail surfaces | `Generate promotion preflight` before eligibility, then `Execute promotion` after a current executable preflight exists | raw JSON, provider IDs, worker internals, and low-level payload diffs | the compare page owns readiness truth once; the confirmation modal restates only the mutation scope, and Monitoring owns run progress after queueing |
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Canonical cross-tenant compare page | Utility / Workspace Decision | Queued execution decision | Execute promotion | explicit selectors plus focused compare or preflight panels and confirmation modal | forbidden | open-source, open-target, return, and open-operation links stay secondary | none; `Execute promotion` is mutating but not destructive and must still use confirmation | `/admin/cross-tenant-compare` | same page with shareable query state and current run handoff | workspace context plus source and target tenant chips | Cross-tenant compare | whether the ready part of the current source selection can be promoted now and what will be excluded | none |
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|---|---|---|---|---|---|---|---|---|---|---|
| Canonical cross-tenant compare page | Workspace operator / MSP operator | Decide whether to queue a bounded promotion run against the target tenant | Canonical decision page | Can I safely apply the ready part of this source selection to the target tenant now, and what will be left out? | source and target summary, ready or blocked counts, manual-mapping and blocked totals, mutation scope, confirmation summary, and latest run handoff | subject-level execution plan, mapping gaps, source and target drill-downs, and follow-up hints | compare readiness, execution availability, and run state | Microsoft tenant target only | Generate promotion preflight, Execute promotion, Open operation, Open source tenant, Open target tenant | Execute promotion (requires confirmation) |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes - one bounded promotion execution planner or bridge plus one queued promotion job
- **New enum/state/reason family?**: yes - one new canonical operation type and one operational control key, but no new persisted lifecycle family or draft state family
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: compare and preflight prove readiness, but operators still cannot execute the bounded next step or observe the result from the same truth.
- **Existing structure is insufficient because**: the current compare slice stops before any `OperationRun`, and the current `RestoreService::executeFromPolicyVersion()` explicitly rejects foreign-tenant versions, so the execution path cannot be implemented as a naive direct call.
- **Narrowest correct implementation**: derive a ready-only execution plan from the current compare and preflight truth, queue one target-tenant-scoped `promotion.execute` run, and translate source content into target-safe write inputs through an existing provider-write seam without introducing draft persistence.
- **Ownership cost**: maintain one new operation vocabulary entry, one bounded execution bridge, one queued worker, small audit metadata expansion, and focused tests.
- **Alternative intentionally rejected**: persisted promotion drafts, scheduled promotions, approval chains, and cross-tenant batch execution are intentionally rejected because they import new truth and operator workflow before the bounded execution seam is proven.
- **Release truth**: current-release workflow gap, not a future-state platform ambition
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, migration shims, and legacy workflow preservation remain out of scope unless the implementation discovers a concrete repo-owned compatibility contract that the current spec missed.
Canonical replacement is preferred over parallel promotion workflows.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Unit, Feature, Browser
- **Validation lane(s)**: fast-feedback, confidence, browser
- **Why this classification and these lanes are sufficient**: unit coverage proves ready-only execution planning and no-draft behavior, focused feature coverage proves page action, authorization, audit, and Monitoring handoff, and one bounded browser smoke proves the confirmation modal and compare-to-operation handoff on the real Filament surface.
- **New or expanded test families**: `tests/Unit/Support/PortfolioCompare/`, `tests/Feature/PortfolioCompare/`, and one new bounded `tests/Browser/PortfolioCompare/` smoke file
- **Fixture / helper cost impact**: moderate; reuse the existing portfolio-compare fixtures, current workspace and tenant setup, and current `OperationRun` assertions rather than introducing a new provider or browser fixture domain
- **Heavy-family visibility / justification**: one new browser smoke file is justified because the feature adds a confirmation modal and Monitoring handoff on a live Filament page
- **Special surface test profile**: standard-native-filament
- **Standard-native relief or required special coverage**: ordinary feature coverage is sufficient for the compare and run-start contract, but the final compare-page handoff to Monitoring must be proven once in the browser
- **Reviewer handoff**: reviewers must confirm the slice stays on one compare page, one run type, zero new draft tables, and shared run UX only
- **Budget / baseline / trend impact**: low to moderate; one new browser smoke file and one new operation type only
- **Escalation needed**: none
- **Active feature PR close-out entry**: Smoke Coverage
- **Planned validation commands**:
- `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`
## Scope Boundaries
### In Scope
- one `Execute promotion` action on the canonical compare page after a current preflight exists
- one explicit confirmation modal that names the source tenant, target tenant, ready counts, excluded counts, and mutation scope
- one queued `promotion.execute` `OperationRun`
- one bounded execution planner or bridge that translates ready subjects into target-safe write inputs
- one truthful Monitoring handoff and run-link continuity
- start and terminal audit metadata for the promotion execution path
- compare-page continuity after queueing, including preserved source, target, and return-state context
### Non-Goals
- persisted compare snapshots or promotion-draft tables
- scheduled or recurring promotion execution
- approval workflows or multi-actor sign-off
- rollback or undo workflows
- multi-target or batch promotion
- mapping automation for manual-mapping-required subjects
- customer-facing promotion surfaces
- cross-workspace execution
- multi-provider execution frameworks
## Assumptions
- the current compare and preflight contracts expose enough stable subject identity to resolve a bounded ready-only execution plan
- the current provider-write seams can accept a target-safe execution payload once the source content is normalized through a bounded bridge
- the existing Monitoring viewer can represent the new run type without a dedicated promotion detail resource
- current queue workers already provide the runtime model needed for one more `OperationRun`-backed promotion job
## Risks
- some subjects marked `ready` by preflight may still fail at execution time because the current target write seam discovers provider constraints later than the read-only preflight can
- the promotion bridge may be tempted to persist a new draft or snapshot entity; that must be rejected unless a separate follow-up spec is created
- run-summary truth can drift if the implementation invents non-canonical summary keys instead of staying on the current `OperationSummaryKeys` set
- a future implementation could try to broaden the slice into approval, rollback, or batch execution; that is out of scope for this spec
## Follow-up Candidates
- manual mapping workflow for `manual_mapping_required` subjects
- portfolio-level batch promotion across many targets
- approval or sign-off gating before execution
- rollback or replay workflow after a promotion run completes
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Queue one bounded promotion from the current preflight (Priority: P1)
As a workspace operator, I want to confirm and queue one bounded promotion from the current compare and preflight context so I can apply only the ready subjects to the target tenant without manual off-platform execution.
**Why this priority**: This is the core value gap left by Spec 043. Without queueable execution, compare remains advisory only.
**Independent Test**: Open the compare page with an executable preflight, confirm the action, and verify that one `promotion.execute` run is queued with ready subjects only.
**Acceptance Scenarios**:
1. **Given** the current preflight contains a mix of `ready`, `blocked`, and `manual_mapping_required` subjects, **When** the operator confirms `Execute promotion`, **Then** only the `ready` subjects enter the queued run context and the excluded subjects remain visible on the compare page.
2. **Given** the current selection and preflight produce no `ready` subjects, **When** the operator tries to execute promotion, **Then** the action is blocked and no `OperationRun` is created.
3. **Given** an active run already exists for the same source, target, subject scope, and executable plan, **When** the operator tries to execute again, **Then** the shared start UX dedupes or reuses the current run instead of creating a second active run.
---
### User Story 2 - Follow the queued promotion through Monitoring with truthful result summary (Priority: P1)
As a workspace operator, I want a canonical run link and truthful result summary after I queue a promotion so I can monitor completion and know what happened without leaving the existing Monitoring path.
**Why this priority**: Execution is unsafe if the operator cannot observe start, progress, and terminal outcome on the existing shared Monitoring path.
**Independent Test**: Queue a promotion run from the compare page and verify that the start result, Monitoring link, and run-summary truth all stay on the shared `OperationRun` contract.
**Acceptance Scenarios**:
1. **Given** a promotion run is successfully queued, **When** the compare page receives the start result, **Then** the operator sees the shared queued feedback and one canonical `Open operation` link.
2. **Given** the promotion run reaches a terminal state, **When** the operator opens the Monitoring detail, **Then** the run summary clearly distinguishes processed, succeeded, failed, and skipped work using the shared summary-key vocabulary.
3. **Given** the run start is blocked by operational control, legitimacy, or target-scoped gating, **When** the operator attempts execution, **Then** they receive the shared blocked feedback and no target mutation begins.
---
### User Story 3 - Preserve portfolio context and execution safety boundaries (Priority: P2)
As a workspace operator, I want the execution path to preserve my compare and return-state context while still enforcing target-safe capability and control boundaries so the workflow stays usable and safe inside the portfolio review loop.
**Why this priority**: The workflow loses value if the operator must rebuild compare context after queueing or if the page hides the reasons why execution is unavailable.
**Independent Test**: Launch compare from the tenant registry, queue or attempt to queue promotion, and verify that source, target, return-state, and safety boundaries all remain intact.
**Acceptance Scenarios**:
1. **Given** the operator launched compare from a registry or portfolio context, **When** they queue a promotion, **Then** the compare page preserves the source tenant, target tenant, governed-subject filters, and return-state continuity.
2. **Given** the operator can view compare but lacks target-tenant manage or workspace baseline-manage access, **When** they reach an otherwise executable compare state, **Then** execution stays visible only as a disabled safety-gated affordance and any forced execution attempt returns `403`.
3. **Given** the source or target tenant becomes inaccessible or stale between preflight and confirmation, **When** the operator attempts execution, **Then** the request is rejected and no `OperationRun` is created.
### Edge Cases
- source and target tenant are the same tenant: reject as invalid input and do not queue a run
- preflight is stale because the compare selection changed after it was generated: require regeneration and do not queue a run
- no ready subjects remain after current truth is re-evaluated at execution time: fail safe and do not queue or start target mutation
- the existing provider-write seam cannot resolve a target-safe payload from the source subject: mark the item failed or skipped inside the run; do not invent a draft state family
- operational control or legitimacy gate blocks `promotion.execute`: surface the shared blocked message and keep the compare state intact
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The canonical `/admin/cross-tenant-compare` page must remain the only operator entrypoint for v1 promotion execution.
- **FR-002**: Promotion execution must require a current compare preview and a current promotion preflight before queueing any target mutation.
- **FR-003**: The page must expose exactly one dominant next action at a time: `Generate promotion preflight` before readiness exists, then `Execute promotion` once the current preflight is executable.
- **FR-004**: `Execute promotion` must require explicit confirmation and must name the source tenant, target tenant, count of ready subjects, count of excluded subjects, and the target-only mutation scope.
- **FR-005**: Only `ready` subjects from the current preflight may enter the queued execution plan; `blocked` and `manual_mapping_required` subjects must stay excluded from the run and visible to the operator.
- **FR-006**: The execution path must create or reuse exactly one canonical `OperationRun` for the current executable plan, using one new canonical operation type for promotion execution.
- **FR-007**: The feature must keep execution truth on existing `OperationRun` state, summary counts, and audit metadata only. No persisted promotion-draft, compare-snapshot, or approval entity may be added.
- **FR-008**: The run-start path must reuse the shared `OperationRun` start UX contract for queued, deduped, blocked, and Monitoring-link behaviors.
- **FR-009**: The implementation must not pretend that a source-tenant `PolicyVersion` belongs to the target tenant. Any bridge from source content to target mutation must preserve tenant-owned truth while still reusing existing provider-write semantics.
- **FR-010**: Run summary counts must stay on canonical summary keys only. Promotion-specific meaning must be expressed through page or Monitoring copy, not a new summary-key family.
- **FR-011**: The feature must record start and terminal audit events for promotion execution with source-tenant, target-tenant, selection, and outcome context.
- **FR-012**: Compare and launch context must remain preserved after queueing, including source tenant, target tenant, governed-subject filters, and return-state payload.
- **FR-013**: Execution authorization must require workspace baseline-manage and target-tenant manage in addition to the existing compare-view permissions.
- **FR-014**: Filament must remain v5 on Livewire v4, and Laravel or Filament service-provider registration must remain unchanged in `apps/platform/bootstrap/providers.php`.
- **FR-015**: No new panel, no new asset registration, no new Laravel or Filament service-provider registration work, and no new globally searchable resource may be introduced for this slice.
### Key Entities *(include if feature involves data)*
- **Cross-tenant compare selection**: existing derived scope of source tenant, target tenant, and governed-subject filters; remains read-only input truth.
- **Promotion preflight**: existing derived readiness artifact grouping subjects into `ready`, `blocked`, and `manual_mapping_required`; becomes the only allowed execution source.
- **Promotion execution plan**: new bounded in-memory or `OperationRun.context` representation of the ready-only mutation plan; not a persisted standalone entity.
- **Promotion execution run**: one canonical `OperationRun` using the new operation type and existing Monitoring viewer.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: An authorized operator can move from current compare preflight to a queued `promotion.execute` run without leaving the canonical compare page.
- **SC-002**: The compare page never queues blocked or manual-mapping-required subjects for target mutation.
- **SC-003**: The queued run can be opened through the shared Monitoring path and exposes truthful processed, succeeded, failed, and skipped counts.
- **SC-004**: The implementation ships without a persisted promotion-draft table, second promotion surface, or second queue family.