TenantAtlas/specs/147-tenant-selector-remembered-context-enforcement/research.md
2026-03-16 23:52:03 +01:00

61 lines
5.6 KiB
Markdown

# Research: Tenant Selector and Remembered Context Enforcement
## Decision 1: Keep active-lane selector eligibility anchored in `TenantOperabilityService`
**Decision**: Use `App\Services\Tenants\TenantOperabilityService` as the single authoritative rule for standard selector membership and require the header selector, choose-tenant page, and selection controllers to converge on that service.
**Rationale**: The service already encapsulates lifecycle-aware operability and exposes `canSelectAsContext()` and `filterSelectable()`. Spec 147 needs consistency, not a second selector rule. Reusing this service directly keeps Spec 143 lifecycle semantics intact and avoids per-surface drift.
**Alternatives considered**:
- Build a new selector-specific eligibility service: rejected because it would duplicate `canSelectAsContext()` semantics and create a new place for lifecycle drift.
- Let each surface query `status = active` independently: rejected because it would bypass existing operability semantics and repeat the exact fragmentation this spec is intended to remove.
## Decision 2: Centralize remembered tenant revalidation in `WorkspaceContext`
**Decision**: Make `App\Support\Workspaces\WorkspaceContext` the authoritative place that validates, returns, clears, and invalidates remembered tenant context for active-lane use.
**Rationale**: `WorkspaceContext` already owns the session map keyed by workspace and already clears stale remembered IDs when workspace or lifecycle checks fail. It is the natural home for enforcing the workspace-scoped preference contract required by Spec 147.
**Alternatives considered**:
- Revalidate remembered tenant context separately in controllers, Livewire pages, and Blade partials: rejected because it preserves scattered raw-session truth assumptions.
- Move remembered-context validation into `OperateHubShell`: rejected because shell resolution should consume validated context, not become the storage owner.
## Decision 3: Keep the custom workspace-first shell instead of reverting to Filament's default tenant switcher
**Decision**: Continue using the existing custom context bar and custom choose-tenant page, but align both to one active-lane rule set.
**Rationale**: The product architecture is explicitly workspace first. Filament's tenant switcher is tenant-menu-centric, while this app already uses a workspace-scoped shell, explicit workspace switching, and canonical workspace-level routes. Docs confirm Filament tenant menu customization exists, but the custom shell is already the correct product pattern here.
**Alternatives considered**:
- Replace the custom selector with Filament's built-in tenant switcher: rejected because it would pull the product back toward a tenant-first mental model and would not solve the workspace-scoped remembered-context rules already present in the app.
- Keep the custom shell but allow it to diverge from the choose-tenant page: rejected because Spec 147 explicitly requires shared semantics between both selector surfaces.
## Decision 4: Treat route authority and shell context as separate layers
**Decision**: Preserve route-authority semantics for tenant-bound and canonical routes and use shell context only for convenience, not for legitimacy.
**Rationale**: `OperationRunPolicy` already authorizes canonical runs by workspace membership, tenant entitlement, and capability checks. `TenantlessOperationRunViewer` already renders non-blocking context banners. Spec 147 should extend this pattern consistently through shell resolution instead of reintroducing tenant-context-coupled validity at the page level.
**Alternatives considered**:
- Use selected tenant equality as a fast validity shortcut for tenant-bound or canonical pages: rejected because it conflicts with Specs 143 and 144 and causes false invalidity.
- Auto-switch the selected tenant to match the route record: rejected because it makes page rendering mutate user context implicitly and hides the distinction between route subject and current active lane.
## Decision 5: “No tenant selected” is a first-class valid shell state
**Decision**: Treat no-selected-tenant as the correct fallback state for workspace-level pages after invalidation, workspace switch, or explicit clear.
**Rationale**: Workspace-level surfaces such as `/admin` and `/admin/operations` are legitimate without an active tenant. Spec 147 requires graceful degradation to a valid workspace-wide view instead of hidden failure when remembered context becomes stale.
**Alternatives considered**:
- Force tenant reselection before allowing workspace-level pages: rejected because it would contradict the workspace-first architecture.
- Keep stale remembered tenant until the user manually clears it: rejected because it would preserve hidden authority and inconsistent page behavior.
## Decision 6: No schema change is required for this feature
**Decision**: Implement Spec 147 as behavior consolidation over existing session-backed workspace tenant preferences and existing user preference persistence paths.
**Rationale**: The app already stores current workspace in session and last tenant IDs in a workspace-keyed session map, with supplemental persistence through `users.last_tenant_id` or `user_tenant_preferences`. The problem is validation and consumption semantics, not missing storage.
**Alternatives considered**:
- Introduce a new tenant-context table or richer persisted context model: rejected because the spec explicitly says remembered tenant context is a convenience preference, not a durable parallel ownership model.
- Persist no tenant preference at all: rejected because remembered context remains a supported convenience feature within the workspace-scoped shell.