TenantAtlas/specs/043-cross-tenant-compare-and-promotion/spec.md
Ahmed Darrazi 6383f205a1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m3s
chore: commit all changes (automated) 2026-04-27T21:17:40Z
2026-04-27 23:17:40 +02:00

294 lines
29 KiB
Markdown

# Feature Specification: Cross-Tenant Compare Preview and Promotion Preflight
**Feature Branch**: `043-cross-tenant-compare-and-promotion`
**Created**: 2026-01-07
**Updated**: 2026-04-27
**Status**: Ready for implementation
**Input**: Refresh existing Spec 043 against `docs/product/spec-candidates.md`, `docs/product/implementation-ledger.md`, and `docs/product/roadmap.md` so the feature becomes a narrow, implementation-ready slice instead of a broad future ambition.
## Spec Candidate Check *(mandatory - SPEC-GATE-001)*
- **Problem**: TenantPilot now has portfolio visibility, triage continuity, and strong tenant-level baseline compare surfaces, but operators still lack one canonical workspace-level path to compare a source tenant to a target tenant and prepare a safe promotion decision.
- **Today's failure**: Operators can see that tenants differ, but they still reconstruct cross-tenant decisions manually across tenant registry, baseline compare, and tenant detail surfaces. Promotion remains a roadmap phrase, not a bounded product workflow.
- **User-visible improvement**: An authorized workspace operator can select a source and target tenant, review a structured compare preview of governed subjects, and generate a read-only promotion preflight that shows what is ready, blocked, or requires manual mapping before any write path exists.
- **Smallest enterprise-capable version**: One canonical `/admin` compare surface, one compare preview builder, one read-only promotion preflight action, deep links back to existing tenant and baseline compare surfaces, and bounded audit metadata for preflight entry points. No actual promotion execution ships in this slice.
- **Explicit non-goals**: No cutover, no write execution, no queue or `OperationRun`, no automatic target remapping of groups/tags/named locations, no cross-workspace compare, no customer-facing compare workspace, no provider marketplace, and no new persisted promotion draft entity.
- **Permanent complexity imported**: One canonical compare page, one narrow compare scope contract, one preview/preflight builder pair, one small audit metadata shape, and focused unit plus feature coverage.
- **Why now**: The implementation ledger explicitly identifies cross-tenant compare and promotion as one of the remaining real product gaps. It is the missing bridge between portfolio visibility and portfolio action.
- **Why not local**: A local compare action on one tenant page would duplicate entitlement, matching, audit, and promotion-readiness logic and would not create a reusable, canonical workspace workflow.
- **Approval class**: Workflow Compression
- **Red flags triggered**: New page + new compare/preflight service pair. Defense: the slice stays read-only, introduces no new table, reuses existing baseline compare and portfolio triage seams, and defers actual execution.
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 1 | Produktnaehe: 1 | Wiederverwendung: 2 | **Gesamt: 10/12**
- **Decision**: approve
## Spec Scope Fields *(mandatory)*
- **Scope**: canonical-view
- **Primary Routes**:
- new canonical admin compare page under `/admin` for cross-tenant compare preview and promotion preflight
- existing `/admin/tenants` portfolio/registry surfaces as launch and return context
- existing tenant detail and baseline compare pages as secondary drill-down targets rather than duplicated local detail panes
- **Data Ownership**:
- compare preview and promotion preflight remain derived from existing tenant-owned inventory, policy-version, and baseline-compare truth
- no new compare snapshot, promotion draft, or mapping table is introduced in v1
- audit remains on the existing workspace audit log only
- **RBAC**:
- non-members or actors outside workspace scope receive `404`
- launch-action visibility requires established workspace context, `Capabilities::WORKSPACE_BASELINES_VIEW` on the workspace, and `Capabilities::TENANT_VIEW` on the launched tenant
- opening the compare page requires established workspace context and `Capabilities::WORKSPACE_BASELINES_VIEW` on the workspace
- loading preview data requires `Capabilities::TENANT_VIEW` on both source and target tenants
- executing promotion preflight requires the preview permissions plus `Capabilities::WORKSPACE_BASELINES_MANAGE` on the workspace
- for established members who can view compare but lack `Capabilities::WORKSPACE_BASELINES_MANAGE`, the preflight action remains visible but disabled with explicit permission help text; server-side attempts still return `403`
- the implementation must stay on existing capability registries instead of raw strings and must not introduce a new promotion capability family for this slice
For canonical-view specs, the spec MUST define:
- **Default filter behavior when tenant-context is active**: if launched from the tenant registry or portfolio-triage context, prefill the launched tenant as the `target tenant`, leave the `source tenant` intentionally user-selected, and preserve a return context token.
- **Explicit entitlement checks preventing cross-tenant leakage**: the compare surface must validate workspace membership first, then validate both source and target tenant entitlement before any preview data loads. Any inaccessible tenant input is treated as not found.
## 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)**: navigation entry points, compare/drill-down actions, audit metadata, and canonical workspace-context pages
- **Systems touched**: `ListTenants`, portfolio-triage state, `CanonicalNavigationContext`, `BaselineCompareLanding`, `BaselineCompareMatrix`, `BaselineCompareService`, `CompareStrategyRegistry`, `WorkspaceAuditLogger`, and `AuditActionId`
- **Existing pattern(s) to extend**: canonical `/admin` workspace-context pages, baseline compare preview patterns, portfolio-triage return-state patterns, and existing workspace audit metadata patterns
- **Shared contract / presenter / builder / renderer to reuse**: `CanonicalNavigationContext`, `ActionSurfaceDeclaration`, `BaselineCompareService`, `BaselineCompareMatrixBuilder`, `CompareStrategyRegistry`, `TenantTriageReviewService`, and `WorkspaceAuditLogger`
- **Why the existing shared path is sufficient or insufficient**: existing tenant-level baseline compare surfaces already solve stable subject matching, result framing, and drill-down semantics, but they are insufficient for cross-tenant compare because they do not accept dual-tenant scope or produce a promotion-readiness preflight.
- **Allowed deviation and why**: none. The new surface should extend current compare and navigation patterns, not invent a parallel compare UX family.
- **Consistency impact**: source tenant, target tenant, compare preview, promotion preflight, blocked reason, and ready/manual mapping language must stay consistent across page copy, modal copy, audit prose, and deep links.
- **Review focus**: reviewers must block new local compare widgets or tenant-specific preflight sidecars that bypass the canonical compare page or its shared preview/preflight services.
## 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?**: no
- **Shared OperationRun UX contract/layer reused**: `N/A`
- **Delegated start/completion UX behaviors**: `N/A`
- **Local surface-owned behavior that remains**: compare preview and promotion preflight stay synchronous and read-only in v1
- **Queued DB-notification policy**: `N/A`
- **Terminal notification path**: `N/A`
- **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 subject identity, compare strategy reuse, promotion preflight reason vocabulary, and operator-facing compare terminology
- **Neutral platform terms preserved or introduced**: `source tenant`, `target tenant`, `governed subject`, `compare preview`, `promotion preflight`, `mapping gap`, and `blocked reason`
- **Provider-specific semantics retained and why**: Microsoft-first policy-type and inventory semantics remain inside existing compare strategy and inventory seams because the repo currently has one real provider domain. They should not leak deeper into the page contract than necessary.
- **Why this does not deepen provider coupling accidentally**: the page and services stay anchored on existing compare registries and inventory identifiers instead of inventing Microsoft-specific page contracts or raw Graph payload handling.
- **Follow-up path**: future multi-provider compare remains a separate follow-up spec 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 primitives | compare preview, navigation, audit-backed preflight action | page, query state, compare summary, modal/action state | no | Reuses baseline compare language and drill-down patterns instead of a custom standalone shell |
| Tenant registry / portfolio launch action | yes | Native Filament action | navigation entry point, contextual launch | table state, query/deep-link state | no | Extends existing portfolio-triage return-state handling |
| Actual promotion execution surface | no | N/A | none | none | no | `N/A - explicitly deferred` |
## 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 target tenant is ready for promotion planning or still blocked by scope and mapping gaps | source/target summary, ready/blocked/manual counts, top blockers, and next action | tenant drill-down, baseline compare drill-down, subject-level diagnostics | Primary because it is the first canonical workspace place where cross-tenant action becomes decidable | Moves from portfolio triage into compare and preflight without manual reconstruction | Replaces cross-page mental diffing with one bounded decision surface |
| Tenant registry / portfolio launch action | Secondary Context | Operator chooses when to leave the tenant registry for compare | current tenant context and preserved return state | compare details live on the compare page | Secondary because it launches the decision surface rather than hosting it | Keeps portfolio review flow intact | Reduces repeated tenant re-selection and filter loss |
## 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/target summary, compare counts, preflight readiness summary, top blocked reasons | subject-level mapping gaps and deep links to tenant-specific evidence | raw payloads remain on existing tenant/baseline pages, not this surface | `Generate promotion preflight` | raw JSON, provider IDs, and low-level evidence stay behind existing detail pages | compare page states the decision truth once; drill-down pages add proof rather than rephrasing the same blocker |
| Tenant registry / portfolio launch action | operator-MSP | current tenant context and compare launch intent | return-state token only | none | `Compare tenants` | any future write action remains absent | launch action does not duplicate compare summaries on the registry row |
## 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 | Draft apply analysis | Generate promotion preflight or open drill-down evidence | explicit selectors plus focused compare/preflight panels | forbidden | drill-down links and secondary navigation stay below the summary/preflight sections | none in v1 | new canonical `/admin` compare route | same page with shareable query state | workspace context plus source/target tenant chips | Cross-tenant compare | whether the target is ready, blocked, or needs manual mapping | none |
| Tenant registry / portfolio launch action | List / Table / Launch Context | Launch context support | Open compare with current tenant prefilled | explicit action from tenant list or triage context | preserved existing row behavior | compare entry is a safe secondary action | none | `/admin/tenants` | compare route | current workspace and tenant | Tenant registry | why the action launches compare, not promotion | existing tenant registry action hierarchy remains valid |
## 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 a target tenant is ready for a later promotion workflow | Canonical decision page | Can this target tenant safely follow the selected source tenant for the chosen governed subjects? | source/target summary, compare counts, blocked reasons, ready/manual counts, and next action | subject-level mappings, stale evidence signals, and deep links to existing tenant compare/detail surfaces | compare state, readiness, mapping confidence, evidence freshness | TenantPilot only in v1 | Generate promotion preflight, open source tenant, open target tenant | none |
| Tenant registry / portfolio launch action | Workspace operator / MSP operator | Start compare from an existing portfolio review path | Registry action | Which tenant should I compare next without losing context? | current tenant identity and compare launch intent | preserved triage filters and return token | launch context only | none | Compare tenants | none |
## Proportionality Review *(mandatory when structural complexity is introduced)*
- **New source of truth?**: no
- **New persisted entity/table/artifact?**: no
- **New abstraction?**: yes - one narrow compare preview builder and one narrow promotion preflight service
- **New enum/state/reason family?**: no new persisted state family; readiness and blocked reasons remain derived from compare/preflight results
- **New cross-domain UI framework/taxonomy?**: no
- **Current operator problem**: operators can identify tenants that need attention but cannot reach a trustworthy cross-tenant decision without manual reconstruction.
- **Existing structure is insufficient because**: existing tenant-level baseline compare pages and portfolio triage state do not support dual-tenant scope or promotion-readiness reasoning.
- **Narrowest correct implementation**: derive compare preview and promotion preflight from existing inventory/baseline truth, keep the page canonical and read-only, and audit only the preflight entry points.
- **Ownership cost**: maintain one compare page, one preview builder, one preflight service, and a handful of focused tests.
- **Alternative intentionally rejected**: actual promotion execution and persisted draft plans were rejected because they would add write risk, queue semantics, and new truth before the compare/preflight workflow is proven.
- **Release truth**: current-release workflow gap, not future-release platform speculation
### Compatibility posture
This feature assumes a pre-production environment.
Backward compatibility, legacy aliases, migration shims, historical fixtures, and compatibility-specific tests are out of scope unless explicitly required by this spec.
Canonical replacement is preferred over preservation.
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
- **Test purpose / classification**: Unit, Feature
- **Validation lane(s)**: fast-feedback, confidence
- **Why this classification and these lanes are sufficient**: unit coverage proves preview matching and promotion-preflight classification without Filament overhead, while focused feature coverage proves page rendering, launch context, audit, and `404`/`403` semantics on the canonical compare surface.
- **New or expanded test families**: one focused `PortfolioCompare` feature family and one focused `Unit/Support/PortfolioCompare` family
- **Fixture / helper cost impact**: moderate; reuse existing tenant, workspace, inventory, baseline compare, and portfolio-triage fixtures instead of adding browser setup or queue scaffolding
- **Heavy-family visibility / justification**: none; do not widen this slice into browser or heavy-governance lanes by default
- **Special surface test profile**: standard-native-filament
- **Standard-native relief or required special coverage**: ordinary feature coverage is sufficient for the page and launch actions; a small unit test set must prove preflight classification and no-write semantics
- **Reviewer handoff**: reviewers must confirm that the slice stays read-only, reuses baseline compare and portfolio seams, preserves deny-as-not-found semantics for inaccessible tenants, and does not smuggle in actual promotion execution
- **Budget / baseline / trend impact**: low increase in unit + feature only
- **Escalation needed**: none
- **Active feature PR close-out entry**: Guardrail
- **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/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`
## Scope Boundaries
### In Scope
- one canonical workspace-context compare page for source/target tenant selection
- read-only compare preview using stable governed-subject identity and existing compare strategy patterns
- one read-only promotion preflight action that classifies ready, blocked, and manual-mapping subjects
- workspace audit metadata for preflight entry points
- launch and return continuity from portfolio-triage/tenant-registry context
- deep links to existing tenant and baseline compare detail pages instead of duplicated proof surfaces
### Non-Goals
- actual promotion execution or target mutation
- queueing, retries, or `OperationRun`
- persisted compare snapshots or promotion draft tables
- automatic mapping writers for groups, scope tags, filters, named locations, or app references
- customer-facing review or compare surfaces
- cross-workspace compare
- multi-provider compare frameworks
## Assumptions
- existing inventory and baseline compare seams already provide enough stable subject identity to drive a first compare preview
- current portfolio-triage return-state patterns are sufficient for launch and back-navigation continuity
- a read-only preflight is valuable before any write path exists and can be audited without introducing a second persistence truth
## Risks
- some compare subjects may still need provider-specific mapping logic before they can produce a trustworthy readiness result
- target inventory freshness or missing evidence may block preflight more often than expected and needs explicit reasoning on the page
- a later implementation could try to add actual promotion execution inside this slice; that must be rejected as scope growth
## Follow-up Candidates
- Cross-tenant promotion execution with preview -> confirmation -> queued run -> verify
- Managed mapping workflows for named locations, assignments, groups, and filters
- Cross-tenant decision inbox integration after compare/preflight exists
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Compare two authorized tenants (Priority: P1)
As a workspace operator, I want to compare one source tenant to one target tenant from a canonical workspace surface so I can see where governed subjects match, differ, or are missing without reconstructing the answer manually.
**Why this priority**: This is the smallest valuable slice that turns portfolio visibility into a concrete operator decision surface.
**Independent Test**: Open the compare page with two authorized tenants, choose governed-subject filters, and verify that the compare preview shows reproducible ready/different/missing results and drill-down links.
**Acceptance Scenarios**:
1. **Given** an operator has access to both selected tenants, **When** they open the compare page and run the preview, **Then** they see a structured compare summary grouped by governed-subject state rather than a raw payload diff.
2. **Given** the same source and target selection, **When** the operator reloads or shares the preview URL, **Then** the compare state is reproducible for the same scoped selection.
3. **Given** the operator selects the same tenant as both source and target, **When** they try to run the preview, **Then** the page rejects the selection as invalid and does not produce compare or preflight output.
---
### User Story 2 - Generate a promotion preflight without writing (Priority: P1)
As a workspace operator, I want a read-only promotion preflight that tells me what is ready, blocked, or needs manual mapping before any cross-tenant write path exists.
**Why this priority**: Promotion language is not trustworthy until the product can explain why a target is or is not ready in a bounded, auditable way.
**Independent Test**: From an authorized compare preview, trigger the preflight action and verify that the page shows readiness counts, blocked reasons, and manual-mapping requirements without mutating source or target tenants.
**Acceptance Scenarios**:
1. **Given** a compare preview contains subjects with stable identity and usable target conditions, **When** the operator generates a promotion preflight, **Then** those subjects appear as ready with a clear explanation.
2. **Given** some subjects are missing identifiers, stale, or blocked by target conditions, **When** the operator generates the preflight, **Then** those subjects appear as blocked or manual-mapping-required with explicit reasons.
3. **Given** the operator generates a preflight, **When** the action completes, **Then** no target mutation, queued run, or provider write occurs.
4. **Given** the operator can view compare but lacks `WORKSPACE_BASELINES_MANAGE`, **When** they reach the compare page, **Then** the preflight action is visibly disabled with permission guidance and any forced request is rejected server-side.
---
### User Story 3 - Launch compare from portfolio context without losing return state (Priority: P2)
As a workspace operator, I want to enter compare from the tenant registry or portfolio-triage context and return without losing my working filters so compare becomes part of the portfolio workflow instead of a detached utility.
**Why this priority**: The workflow is much less useful if compare starts from scratch and breaks the operator's portfolio-review context.
**Independent Test**: Launch compare from the tenant registry with active triage filters, verify one tenant is prefilled, and verify the return path restores the prior registry state.
**Acceptance Scenarios**:
1. **Given** the tenant registry has active portfolio-triage filters, **When** the operator launches compare from a tenant row or contextual action, **Then** the compare page preserves a return token and prefills the launched tenant as the `target tenant`.
2. **Given** the operator returns from compare, **When** the registry reloads, **Then** the prior triage filters are restored.
### Edge Cases
- source and target tenant are the same tenant: reject the selection as invalid input and do not compute preview or preflight
- source and target tenants belong to different workspaces
- one selected tenant is no longer visible or never belonged to the actor's scope
- compare subjects have ambiguous identity or duplicate matches
- target evidence is stale or missing, making readiness impossible to prove
## Requirements *(mandatory)*
### Functional Requirements
- **FR1**: The feature MUST provide one canonical workspace-context compare surface for selecting source and target tenants.
- **FR2**: The feature MUST enforce workspace membership and source/target tenant entitlement before loading compare data; inaccessible tenants resolve as `404`.
- **FR3**: The compare preview MUST use stable governed-subject identity and existing inventory/baseline compare seams rather than raw JSON diffing.
- **FR4**: The compare preview MUST stay read-only and MUST deep-link to existing tenant or baseline detail surfaces for proof instead of duplicating raw diagnostics locally.
- **FR5**: The feature MUST provide a read-only promotion preflight action that classifies subjects as ready, blocked, or manual-mapping-required.
- **FR6**: The preflight MUST NOT execute a target write, queue a run, or persist a promotion draft artifact.
- **FR7**: The preflight MUST explain blocked and manual states with explicit operator-readable reasons.
- **FR8**: The feature MUST reuse existing capability registries with this exact split: page access = `WORKSPACE_BASELINES_VIEW`, preview data = `TENANT_VIEW` on both tenants, preflight execution = `WORKSPACE_BASELINES_MANAGE`.
- **FR9**: The feature MUST preserve launch and return continuity from the tenant registry / portfolio-triage path.
- **FR10**: The feature MUST record bounded workspace audit metadata for promotion-preflight entry points only.
- **FR11**: The compare page MUST reject same-tenant selection before preview or preflight runs.
### Non-Functional Requirements
- **NFR1**: The feature MUST preserve workspace and tenant isolation and MUST NOT leak source or target hints to unauthorized actors.
- **NFR2**: The compare page MUST remain operator-first, decision-first, and must not expose raw payloads by default.
- **NFR3**: The implementation MUST remain Filament-native on Livewire v4 and must not introduce a second compare shell or custom status framework.
- **NFR4**: The slice MUST not introduce new assets or new globally searchable resources.
## Success Criteria
- **SC1**: An authorized operator can produce a cross-tenant compare preview from one canonical page without switching across multiple tenant detail surfaces.
- **SC2**: The same source, target, and filter selection produces reproducible compare output.
- **SC3**: A promotion preflight clearly separates ready, blocked, and manual subjects without performing any write.
- **SC4**: Unauthorized source/target combinations remain deny-as-not-found.
- **SC5**: View-only members can inspect compare results but cannot execute preflight, and the UI makes that boundary explicit.
## Related Specs
- Program: `specs/039-inventory-program/spec.md`
- Core: `specs/040-inventory-core/spec.md`
- UI: `specs/041-inventory-ui/spec.md`
- Drift: `specs/044-drift-mvp/spec.md`
- Foundation follow-up context: `docs/product/spec-candidates.md` (`Cross-Tenant Compare and Promotion v1`)