230 lines
19 KiB
Markdown
230 lines
19 KiB
Markdown
# 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. |