TenantAtlas/specs/292-workspace-tenant-closure/plan.md
Ahmed Darrazi adf9237152
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m32s
feat: implement workspace and tenant closure lifecycle
2026-05-07 15:08:20 +02:00

302 lines
24 KiB
Markdown

# Implementation Plan: Workspace & Tenant Closure Lifecycle v1
**Branch**: `292-workspace-tenant-closure` | **Date**: 2026-05-07 | **Spec**: [spec.md](./spec.md)
**Input**: Feature specification from `specs/292-workspace-tenant-closure/spec.md`
## Summary
Prepare one bounded lifecycle runtime follow-through on top of the Spec 262 taxonomy foundation plus the already-real tenant operability and commercial lifecycle seams. The narrow implementation path is to add explicit workspace closed truth on `Workspace`, explicit removed-from-workspace truth on `ManagedEnvironment`, keep commercial suspension as a separate shared gate, and reuse the existing system directory, admin resource, chooser, and canonical historical viewer surfaces instead of opening a new workflow console.
This slice stays deliberately narrow. Filament remains v5 on Livewire v4, panel-provider registration stays in `apps/platform/bootstrap/providers.php`, no new globally searchable resource is introduced, and no asset registration change is expected. The plan does not reopen the broader taxonomy, does not implement purge or export-before-delete, does not add payment-provider or billing-portal behavior, and does not add a new `OperationRun` family.
## Inherited Baseline / Explicit Delta
### Inherited baseline
- `Workspace` already carries `archived_at`, and `WorkspaceContext::isWorkspaceSelectable()` already uses that archived truth to prevent invalid workspace selection.
- `ManagedEnvironment` already carries tenant lifecycle truth, and `TenantLifecycle` plus `TenantOperabilityService` already distinguish active context from onboarding or archive semantics.
- `WorkspaceCommercialLifecycleResolver` already maps commercial truth into `SUSPENDED_READ_ONLY` versus normal workspace posture for high-impact actions.
- `EnsureWorkspaceSelected`, `EnsureFilamentTenantSelected`, and `DenyNonMemberTenantAccess` already provide the current context-recovery and deny-as-not-found seams.
- `WorkspaceMembershipManager`, `TenantMembershipManager`, `WorkspaceAuditLogger`, and current tenant audit paths already provide bounded audit-safe mutation seams.
- Existing admin and system surfaces already exist at `ViewWorkspace`, `ViewTenant`, `ViewRun`, `ChooseWorkspace`, `ChooseTenant`, `WorkspaceResource`, `TenantResource`, and `WorkspaceSettings`.
### Explicit delta in this plan
- Add explicit closure truth to `Workspace` using bounded persisted fields for closed posture, actor, and reason.
- Add explicit removed-from-workspace truth to `ManagedEnvironment` using bounded persisted fields for removal posture, actor, and reason.
- Introduce one bounded `WorkspaceLifecycleService` to orchestrate close/reopen and remove/restore without opening a generic lifecycle framework.
- Keep `archived`, `suspended read-only`, `closed`, and `removed from workspace` as four distinct meanings with separate badges, copy, and blocked-action explanations.
- Update chooser recovery, tenant-context legitimacy, and canonical historical viewers so historical records remain readable while invalid active context is cleared explicitly.
- Keep all new lifecycle mutations inside current admin and system surfaces and current audit infrastructure.
## Technical Context
**Language/Version**: PHP 8.4, Laravel 12
**Primary Dependencies**: Filament v5, Livewire v4, Pest v4, existing `WorkspaceContext`, `TenantOperabilityService`, `WorkspaceCommercialLifecycleResolver`, current audit infrastructure, current Filament admin and system pages
**Storage**: PostgreSQL via targeted new lifecycle fields on existing `workspaces` and `managed_environments` tables plus existing audit and history tables
**Testing**: Pest v4 focused `Feature` coverage
**Validation Lanes**: fast-feedback, confidence
**Target Platform**: Laravel monolith in `apps/platform` across the existing admin and system Filament panels
**Project Type**: Web application (Laravel monolith with Filament panels)
**Performance Goals**: DB-only lifecycle gating, no new queue family, no new Graph calls, no new browser-only proof requirement
**Constraints**: no purge, no export-before-delete, no payment-provider or billing workflow, no new global-search resource, no new panel, no lifecycle engine, no asset-registration change
**Scale/Scope**: 2 existing persisted records gain bounded lifecycle truth, 1 bounded service seam, 6 existing operator surfaces, and focused feature-test extensions
## Likely Affected Repo Surfaces
- `apps/platform/app/Models/Workspace.php`
- `apps/platform/app/Models/ManagedEnvironment.php`
- `apps/platform/database/migrations/*_add_workspace_closure_fields.php`
- `apps/platform/database/migrations/*_add_managed_environment_workspace_removal_fields.php`
- `apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php`
- `apps/platform/app/Support/Workspaces/WorkspaceContext.php`
- `apps/platform/app/Services/Tenants/TenantOperabilityService.php`
- `apps/platform/app/Services/Entitlements/WorkspaceCommercialLifecycleResolver.php`
- `apps/platform/app/Http/Middleware/EnsureWorkspaceSelected.php`
- `apps/platform/app/Support/Middleware/EnsureFilamentTenantSelected.php`
- `apps/platform/app/Support/Middleware/DenyNonMemberTenantAccess.php`
- `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php`
- `apps/platform/app/Filament/System/Pages/Directory/ViewTenant.php`
- `apps/platform/app/Filament/System/Pages/Ops/ViewRun.php`
- `apps/platform/app/Filament/Pages/ChooseWorkspace.php`
- `apps/platform/app/Filament/Pages/ChooseTenant.php`
- `apps/platform/app/Filament/Pages/Settings/WorkspaceSettings.php`
- `apps/platform/app/Filament/Resources/Workspaces/WorkspaceResource.php`
- `apps/platform/app/Filament/Resources/Workspaces/Pages/ViewWorkspace.php`
- `apps/platform/app/Filament/Resources/TenantResource.php`
- `apps/platform/app/Services/Audit/WorkspaceAuditLogger.php`
- current tenant audit logging path where tenant membership and lifecycle actions are recorded
- focused feature tests under `apps/platform/tests/Feature/System/`, `apps/platform/tests/Feature/Filament/Resources/`, and `apps/platform/tests/Feature/Filament/Pages/`
## Lifecycle Truth Fit
- Treat workspace closure as a dedicated workspace-owned lifecycle posture, not as archive and not as commercial suspension.
- Treat tenant removal from workspace as a dedicated workspace-owned posture on the tenant record, not as tenant archive and not as provider absence.
- Keep commercial suspension in `WorkspaceCommercialLifecycleResolver` as the shared source of `SUSPENDED_READ_ONLY`; closure logic must compose with it, not replace it.
- Preserve current tenant lifecycle and provider-missing semantics; the feature only adds workspace-governed closure and removal truth.
- Preserve historical readability:
- closed workspace -> not selectable as current workspace, read-only historical inspection remains available to entitled actors
- removed tenant -> not selectable as current tenant, canonical historical inspection remains available to entitled actors
- suspended read-only -> current workspace remains inspectable and current tenant may remain visible, but mutations and starts stay blocked per existing commercial rules
## Data & Query Fit
- Extend `workspaces` with bounded closure fields only. The expected shape is a timestamp, actor reference, and reason text rather than a broad lifecycle ledger.
- Extend `managed_environments` with bounded removed-from-workspace fields only. The expected shape mirrors the workspace closure truth and remains reversible.
- Add only the indexes needed to filter active versus closed workspaces and active versus removed tenants.
- Keep lifecycle truth on the primary records instead of introducing a new history table. Audit remains the historical source for who changed posture and when.
- Preserve existing ownership rules: workspaces stay workspace-owned, tenants stay workspace-owned, and no cross-workspace migration or copy is introduced.
## UI / Filament & Livewire Fit
- Existing operator-facing surfaces remain native Filament surfaces under Livewire v4; this slice must stay inside those surfaces.
- `ViewWorkspace` in the system plane keeps one dominant action: `Close workspace` or `Reopen workspace`.
- Admin workspace detail gains read-only posture summary only; no second mutation plane for closure appears on `/admin`.
- Managed tenant list keeps row click as the inspect model and moves remove or restore into `More`; managed tenant detail may expose `Remove tenant` or `Restore tenant` as the dominant lifecycle action.
- Chooser surfaces remain chooser surfaces. They gain explicit recovery messaging, not a new workflow.
- Canonical historical viewers gain posture badges or supporting text only; they do not become mutation surfaces.
- Confirmation modals and success or error notifications on the in-scope action surfaces must reuse the canonical verbs `Close workspace`, `Reopen workspace`, `Remove tenant`, and `Restore tenant`.
- No new globally searchable Filament resource is introduced, so there is no new global-search Edit or View page requirement to satisfy.
- Provider registration remains unchanged in `apps/platform/bootstrap/providers.php`, and no new asset strategy is planned. If future shared assets ever become necessary, deployment remains the normal `cd apps/platform && php artisan filament:assets` path.
## RBAC / Policy Fit
- Workspace and tenant membership remain the isolation boundaries.
- Wrong-plane actors and non-members stay `404`; in-scope actors missing capability stay `403`.
- Closure and removal actions remain server-side authorized and confirmation-protected.
- Platform closure uses the current platform workspace-governance seam or the narrowest bounded extension of it. Tenant removal uses the current workspace owner or equivalent tenant-governance seam.
- The exact write-side enforcement seam is `apps/platform/app/Services/Workspaces/WorkspaceLifecycleService.php`, and the in-scope mutation entry points are the system workspace detail actions in `apps/platform/app/Filament/System/Pages/Directory/ViewWorkspace.php` and the tenant lifecycle actions in `apps/platform/app/Filament/Resources/TenantResource.php`.
- Unsafe close or remove attempts must preserve current guard failures with a clear operator-facing reason and no partial lifecycle mutation.
- Closure or removal posture must never substitute for authorization. A historically readable record still requires entitlement.
- Tenant-context routes under `/admin/t/{tenant}/...` must reject removed tenants as active context targets while preserving canonical viewers that are not tenant-context routes.
## Audit / Logging Fit
- Every close/reopen and remove/restore mutation must write an audit event with actor, old posture, new posture, timestamp, and reason.
- Existing audit infrastructure must be reused rather than opening a new lifecycle audit subsystem.
- Historical viewers should surface closure or removal context through current summary and linked audit rather than duplicating the full audit payload.
- Blocked active-context starts caused by closure or removal do not need a new audit family in this slice unless existing blocked-action logging already applies.
## UI / Surface Guardrail Plan
- **Guardrail scope**: changed surfaces
- **Native vs custom classification summary**: native Filament
- **Shared-family relevance**: status messaging, chooser recovery, detail actions, canonical historical viewers, audit-backed lifecycle copy
- **State layers in scope**: shell, page, detail
- **Audience modes in scope**: operator-MSP, support-platform
- **Decision/diagnostic/raw hierarchy plan**: decision-first, diagnostics-second, support-raw-third
- **Raw/support gating plan**: raw audit and low-level identifiers remain secondary and platform-only where applicable
- **One-primary-action / duplicate-truth control**: system workspace detail keeps one dominant closure action; tenant detail keeps one dominant remove/restore action; chooser recovery surfaces keep one next step
- **Handling modes by drift class or surface**: review-mandatory
- **Repository-signal treatment**: review-mandatory now; hard-stop candidate if implementation adds a second closure plane or local lifecycle vocabulary
- **Special surface test profiles**: standard-native-filament, global-context-shell, shared-detail-family
- **Required tests or manual smoke**: functional-core, state-contract
- **Exception path and spread control**: none planned; any lifecycle dashboard, broad workbench, or browser-heavy proof demand is out-of-scope drift
- **Active feature PR close-out entry**: Guardrail
## Shared Pattern & System Fit
- **Cross-cutting feature marker**: yes
- **Systems touched**: `WorkspaceContext`, `TenantOperabilityService`, `WorkspaceCommercialLifecycleResolver`, chooser pages, system directory pages, admin workspace and tenant resources, audit logging, and canonical historical viewers
- **Shared abstractions reused**: current context manager, current commercial lifecycle resolver, current tenant operability service, current audit infrastructure, `BadgeCatalog` and `BadgeRenderer`, current Filament action-surface patterns
- **New abstraction introduced? why?**: one bounded `WorkspaceLifecycleService`, because close/reopen and remove/restore each require coordinated record mutation, audit logging, and shared blocked-action consequences across multiple surfaces
- **Why the existing abstraction was sufficient or insufficient**: current services already decide selectability, commercial blocking, and audit shape, but no existing service owns the explicit closure or removal mutation path or the shared write-side behavior for it
- **Bounded deviation / spread control**: no second closure orchestration helper is allowed outside the bounded service path
## OperationRun UX Impact
- **Touches OperationRun start/completion/link UX?**: yes, by reuse and blocking only
- **Central contract reused**: current shared `OperationRun` start UX and current canonical monitoring pages remain authoritative
- **Delegated UX behaviors**: blocked starts in closed workspaces or removed-tenant contexts must fail before enqueue on `apps/platform/app/Filament/Widgets/Tenant/TenantReviewPackCard.php`; existing run links and canonical historical viewers remain unchanged for already-created runs
- **Surface-owned behavior kept local**: close/reopen and remove/restore input collection plus impact summaries only
- **Queued DB-notification policy**: unchanged
- **Terminal notification path**: unchanged central lifecycle mechanism
- **Exception path**: none
## Provider Boundary & Portability Fit
- **Shared provider/platform boundary touched?**: no
- **Provider-owned seams**: none in this slice
- **Platform-core seams**: workspace closure truth, tenant removal truth, chooser recovery, and historical record readability
- **Neutral platform terms / contracts preserved**: `workspace`, `tenant`, `closed`, `removed from workspace`, `suspended read-only`, `history`
- **Retained provider-specific semantics and why**: none
- **Bounded extraction or follow-up path**: provider-missing and other provider lifecycle work stays in separate follow-up specs
## Constitution Check
*GATE: Must pass before implementation begins and again before merge.*
- Inventory-first / snapshot truth: PASS. No inventory or snapshot source is reinterpreted.
- Read/write separation: PASS. The only new writes are explicit lifecycle mutations with confirmation, audit, and test requirements.
- Graph contract path: PASS. No Graph calls are introduced.
- Deterministic capabilities: PASS. Existing capability registries remain authoritative.
- Workspace and tenant isolation: PASS. Existing 404 and 403 rules remain and are reinforced.
- RBAC-UX plane separation: PASS. System owns workspace closure mutation; admin remains read-only for workspace closure and bounded for tenant removal inside the current workspace.
- Destructive action discipline: PASS. Close/reopen and remove/restore are destructive-like and must remain confirmation-protected.
- Global search safety: PASS. No new searchable resource is introduced.
- OperationRun / Ops-UX: PASS by reuse only. No new run family is added, and blocked starts stay pre-enqueue.
- Data minimization: PASS. Only bounded lifecycle truth fields are added on existing records.
- Test governance: PASS. Focused feature lanes are the narrowest honest proof.
- Proportionality / no premature abstraction: PASS. One bounded service is the narrowest write-side seam; no generic lifecycle framework is planned.
- Persisted truth: PASS. Closure and removal posture are real product truth with independent lifecycle and audit need.
- Behavioral state: PASS. Closed and removed-from-workspace change chooser behavior, mutation legality, and historical-view posture.
- Shared pattern first / UI semantics / Filament-native UI: PASS. Existing chooser, audit, badge, and Filament surfaces remain the shared path.
- Provider boundary: PASS. No provider-specific semantics are added.
- Filament / Laravel planning contract: PASS. Filament stays v5 on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, no globally searchable resource is added, and no asset registration change is planned.
**Gate evaluation**: PASS.
## Test Governance Check
- **Test purpose / classification by changed surface**: `Feature` for system workspace governance, admin read-only posture, managed-tenant remove/restore, chooser recovery, and canonical historical viewer legitimacy
- **Affected validation lanes**: fast-feedback, confidence
- **Why this lane mix is the narrowest sufficient proof**: the slice is about integrated route, page, capability, chooser, and audit behavior; unit-only proof would miss the real lifecycle consequences
- **Narrowest proving command(s)**:
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/System/Directory/ViewWorkspaceClosureTest.php tests/Feature/System/Ops/ClosedWorkspaceHistoricalAccessTest.php`
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/Resources/Workspaces/WorkspaceClosureStatusTest.php tests/Feature/Filament/Resources/TenantResource/TenantWorkspaceRemovalTest.php tests/Feature/Filament/Pages/WorkspaceContextClosureRecoveryTest.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**: moderate but contained; setup needs explicit workspace and tenant membership plus lifecycle posture states, but no new provider, browser, or heavy defaults
- **Expensive defaults or shared helper growth introduced?**: no
- **Heavy-family additions, promotions, or visibility changes**: none planned
- **Surface-class relief / special coverage rule**: standard-native-filament relief for the system and admin detail surfaces; global-context-shell coverage for chooser recovery; shared-detail-family coverage for historical viewers
- **Closing validation and reviewer handoff**: reviewers should rely on the exact commands above, verify that no second mutation plane or lifecycle vocabulary appeared, and confirm that blocked starts stay pre-enqueue while historical viewers stay readable
- **Budget / baseline / trend follow-up**: none expected beyond a small feature-local feature-test increase
- **Review-stop questions**: did the slice collapse closure into archive or suspension, did it break canonical history viewers, did it add a second mutation plane, and did it widen into purge/export/billing scope
- **Escalation path**: `document-in-feature` for contained naming or surface drift; `reject-or-split` if the slice widens into broader offboarding, purge, or billing workflows
- **Active feature PR close-out entry**: Guardrail
- **Why no dedicated follow-up spec is needed**: the remaining adjacent work is already known and separately named; this slice is the bounded workspace and tenant closure follow-through itself
- **Test-governance outcome**: keep
## Rollout Considerations
- Land persistence and the bounded service seam first, then workspace closure surfaces plus chooser recovery, then tenant removal surfaces plus canonical historical viewer polish.
- Keep admin workspace detail read-only for closure mutation throughout the slice.
- Keep commercial suspension readable and separate from closure throughout the rollout.
- Keep no asset changes, no provider registration changes, no new panel work, and no global-search work.
## Risk Controls
- Reject any implementation that reuses `archived_at` or commercial suspension as the new closure truth.
- Reject any implementation that deletes memberships, tenants, workspaces, or historical records as part of close/reopen or remove/restore.
- Reject any implementation that makes `/admin` a second workspace-closure mutation plane.
- Reject any implementation that turns the slice into export, purge, payment-provider, or portal work.
- Reject browser-heavy proof as the default validation lane.
## Project Structure
### Documentation (this feature)
```text
specs/292-workspace-tenant-closure/
├── plan.md
├── spec.md
└── tasks.md
```
### Source Code (expected implementation surfaces)
```text
apps/platform/
├── app/
│ ├── Filament/
│ │ ├── Pages/
│ │ │ ├── ChooseTenant.php
│ │ │ ├── ChooseWorkspace.php
│ │ │ └── Settings/
│ │ │ └── WorkspaceSettings.php
│ │ ├── Resources/
│ │ │ ├── TenantResource.php
│ │ │ └── Workspaces/
│ │ │ ├── WorkspaceResource.php
│ │ │ └── Pages/
│ │ │ └── ViewWorkspace.php
│ │ └── System/Pages/
│ │ ├── Directory/
│ │ │ ├── ViewTenant.php
│ │ │ └── ViewWorkspace.php
│ │ └── Ops/
│ │ └── ViewRun.php
│ ├── Http/Middleware/
│ │ └── EnsureWorkspaceSelected.php
│ ├── Models/
│ │ ├── ManagedEnvironment.php
│ │ └── Workspace.php
│ ├── Services/
│ │ ├── Audit/
│ │ │ └── WorkspaceAuditLogger.php
│ │ ├── Entitlements/
│ │ │ └── WorkspaceCommercialLifecycleResolver.php
│ │ ├── Tenants/
│ │ │ └── TenantOperabilityService.php
│ │ └── Workspaces/
│ │ └── WorkspaceLifecycleService.php
│ └── Support/
│ ├── Middleware/
│ │ ├── DenyNonMemberTenantAccess.php
│ │ └── EnsureFilamentTenantSelected.php
│ └── Workspaces/
│ └── WorkspaceContext.php
├── database/
│ └── migrations/
└── tests/
└── Feature/
├── Filament/
└── System/
```
**Structure Decision**: Keep the slice inside the existing Laravel monolith and current Filament admin plus system surfaces. Add only targeted lifecycle fields, one bounded service, and focused feature tests.
## Complexity Tracking
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| Bounded lifecycle write service | Close/reopen and remove/restore need coordinated mutation, audit, and shared blocked-action behavior | Controller or page-local closures would duplicate audit and lifecycle consequences across multiple surfaces |
## Proportionality Review
- **Current operator problem**: Operators cannot deliberately close a workspace or remove a tenant from a workspace while keeping history readable and posture explicit.
- **Existing structure is insufficient because**: archive and commercial suspension represent different meanings and cannot safely absorb closure or removal truth.
- **Narrowest correct implementation**: add bounded fields on existing records, route writes through one bounded service, and reuse current chooser, audit, and Filament surfaces.
- **Ownership cost created**: migrations, one service, shared status and chooser updates, and feature tests.
- **Alternative intentionally rejected**: generic lifecycle engine, new closure history tables, or overloading archive and suspension.
- **Release truth**: current-release truth.