TenantAtlas/specs/216-provider-dispatch-gate/plan.md
ahmido a089350f98
Some checks failed
Main Confidence / confidence (push) Failing after 49s
feat: unify provider-backed action dispatch gating (#255)
## Summary
- unify provider-backed action starts behind the shared provider dispatch gate and shared start-result presenter
- align tenant, onboarding, provider-connection, restore, directory, and monitoring surfaces with the same blocked, deduped, scope-busy, and accepted semantics
- include the spec kit artifacts for spec 216 and the regression fixes that brought the full suite back to green

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/RestoreRunIdempotencyTest.php tests/Feature/ExecuteRestoreRunJobTest.php tests/Feature/Restore/RestoreRunProviderStartTest.php tests/Feature/Hardening/ExecuteRestoreRunJobGateTest.php tests/Feature/Hardening/BlockedWriteAuditLogTest.php tests/Feature/Onboarding/OnboardingDraftLifecycleTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec177InventoryCoverageTruthSmokeTest.php`
- `cd apps/platform && ./vendor/bin/sail artisan test --compact`

## Notes
- branch: `216-provider-dispatch-gate`
- commit: `34230be7`

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #255
2026-04-20 06:52:38 +00:00

25 KiB

Implementation Plan: Provider-Backed Action Preflight and Dispatch Gate Unification

Branch: 216-provider-dispatch-gate | Date: 2026-04-19 | Spec: spec.md Input: Feature specification from /specs/216-provider-dispatch-gate/spec.md

Summary

Unify every covered operator-triggered provider-backed start behind the existing ProviderOperationStartGate so preventable provider blockers, same-operation dedupe, and cross-operation scope conflicts are resolved before queue admission instead of surfacing later as execution failures. The implementation extends the current gate and registry, adds one thin shared start-result presentation helper over the existing Ops UX stack, standardizes the public start wording as queued, already running, scope busy, and blocked over the canonical accepted, deduped, scope busy, and blocked result model, pins accepted work to an explicit provider_connection_id, keeps scheduled and system-run Monitoring language compatible without widening click-time UX scope, and migrates the first-slice tenant, provider-connection, restore, directory-sync, and onboarding start surfaces without adding new persistence or a second orchestration framework.

Technical Context

Language/Version: PHP 8.4.15, Laravel 12, Filament v5, Livewire v4
Primary Dependencies: Filament Resources/Pages/Actions, Livewire 4, Pest 4, ProviderOperationStartGate, ProviderOperationRegistry, ProviderConnectionResolver, OperationRunService, ProviderNextStepsRegistry, ReasonPresenter, OperationUxPresenter, OperationRunLinks
Storage: PostgreSQL via existing operation_runs, provider_connections, managed_tenant_onboarding_sessions, restore_runs, and tenant-owned runtime records; no new tables planned
Testing: Pest unit and focused feature tests for gate transitions, Filament action hosts, onboarding wizard flows, and Ops UX/run-detail alignment
Validation Lanes: fast-feedback, confidence
Target Platform: Laravel admin web app under Sail, rendered through Filament on Linux containers
Project Type: Monorepo with one Laravel platform application in apps/platform plus docs/spec artifacts at repository root
Performance Goals: Start preflight remains DB-only at click time, performs no Graph call before queue admission, adds no new remote latency source to the click-time path, and admits at most one accepted provider-backed run per protected scope
Constraints: No new provider-start framework, no parallel queue-admission path, no new persistence, no operation-type naming rewrite, no inline remote work on start surfaces, no drift from RBAC 404/403 semantics, no drift from Ops-UX 3-surface feedback, and no hidden bypass of the gate on covered start surfaces
Scale/Scope: First slice covers every current operator-triggered provider-backed start reachable from tenant-scoped surfaces, provider-connection surfaces, and onboarding: tenant verification, provider-connection check/inventory/compliance, restore execute, directory groups sync, role definitions sync, onboarding verification, and onboarding bootstrap

Filament v5 Implementation Contract

  • Livewire v4.0+ compliance: Preserved. This feature stays within existing Filament v5 and Livewire v4 patterns and does not introduce any Livewire v3-era APIs.
  • Provider registration location: Unchanged. No panel/provider work is planned; existing panel providers remain registered in bootstrap/providers.php, not bootstrap/app.php.
  • Global search coverage:
    • TenantResource remains globally searchable and already has both view and edit pages.
    • EntraGroupResource remains tenant-scoped for global search and already has a view page.
    • ProviderConnectionResource keeps global search disabled ($isGloballySearchable = false).
    • RestoreRunResource is not being newly enabled for global search in this feature; its existing view page remains available if current search behavior references it.
  • Destructive actions: No new destructive actions are introduced. Restore execution remains a destructive-like flow that continues to rely on existing confirmation and authorization requirements; existing connection lifecycle actions remain Action::make(...)->action(...) based and keep ->requiresConfirmation() where already required.
  • Asset strategy: No new panel or shared assets are planned. Deployment expectations remain unchanged, including cd apps/platform && php artisan filament:assets when registered Filament assets change.
  • Testing plan: Keep one supporting unit seam for the gate and shared presentation mapping, then prove the feature through focused Filament/Livewire feature coverage on tenant, provider-connection, onboarding, restore, directory-sync, and run-detail alignment. No browser lane or new heavy-governance family is planned.

UI / Surface Guardrail Plan

  • Guardrail scope: Changed surfaces across existing tenant-scoped provider-backed starts, provider-connection start actions, restore execute, directory sync starts, onboarding provider steps, and canonical Monitoring run drill-in language
  • Native vs custom classification summary: Mixed shared-family change using native Filament actions, native notifications, and existing shared Ops UX helpers; no new custom shell or bespoke panel surface
  • Shared-family relevance: Shared provider action family across TenantResource, TenantVerificationReport, ProviderConnectionResource, RestoreRunResource, ListEntraGroups, ManagedTenantOnboardingWizard, and related run-detail explanation surfaces
  • State layers in scope: page, detail, wizard-step, and existing run-link drill-in; no new URL-query or shell ownership added
  • Handling modes by drift class or surface: Hard-stop for blocked preflight and protected-scope conflicts; review-mandatory for any remaining direct dispatch path on a covered surface
  • Repository-signal treatment: Review-mandatory because the feature changes a shared action-host family and canonical run-entry semantics
  • Special surface test profiles: standard-native-filament, workflow / wizard, monitoring-state-page
  • Required tests or manual smoke: functional-core, state-contract, manual-smoke
  • Exception path and spread control: One named exception boundary only: onboarding bootstrap currently batches multiple provider-backed operations and must be normalized to one accepted protected-scope run without introducing a new orchestration framework
  • Active feature PR close-out entry: Guardrail

Constitution Check

GATE: Passed before Phase 0 research. Re-checked after Phase 1 design: still passed with one bounded helper addition and no new persisted truth.

Gate Status Plan Notes
Inventory-first / read-write separation PASS The feature changes start admission and feedback only. Accepted work still executes asynchronously; restore keeps preview/confirmation/audit semantics and no new snapshot or runtime truth is introduced.
Single Graph contract path / no inline remote work PASS Start surfaces remain authorize + preflight + enqueue only. Existing queued jobs continue to own Graph calls through the current provider abstractions; no render-time or click-time Graph call is added.
RBAC, workspace isolation, tenant isolation PASS Covered starts remain inside existing tenant/workspace scopes. Non-members remain 404, members missing capability remain 403, and provider readiness messaging must not leak cross-tenant active runs or connection identity.
Run observability / Ops-UX 3-surface feedback PASS Covered starts continue to create or reuse canonical OperationRun truth. Accepted starts keep toast-only intent feedback, progress stays in active widgets/run detail, and terminal DB notifications remain owned by OperationRunService for accepted work only.
Proportionality / no premature abstraction / few layers PASS The plan extends the existing gate and registry and adds one thin presentation helper to absorb five-plus duplicated local result branches. No second gate, coordinator framework, or persisted summary layer is introduced.
UI semantics / Filament-native action discipline PASS Existing action hosts remain in place. The feature standardizes start-result language without adding new navigation models, new shell surfaces, or page-local status frameworks.
Test governance PASS Proof stays in unit plus focused feature lanes with explicit provider/tenant context opt-in. No new browser or heavy-governance family is required.

Test Governance Check

  • Test purpose / classification by changed surface: Unit for gate transition and presentation mapping seams; Feature for tenant/provider/onboarding/restore/directory start surfaces and accepted-run reason alignment; Heavy-Governance: none; Browser: none
  • Affected validation lanes: fast-feedback, confidence
  • Why this lane mix is the narrowest sufficient proof: The business truth is server-side preflight, authorization, dedupe/scope busy handling, queue admission, and consistent operator feedback on existing action hosts. That behavior is fully provable with direct feature tests and one small unit seam; browser coverage would add cost without proving unique behavior.
  • Narrowest proving command(s):
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
    • export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Providers/ProviderOperationStartResultPresenterTest.php tests/Feature/ProviderConnections/ProviderDispatchGateStartSurfaceTest.php tests/Feature/Tenants/TenantProviderBackedActionStartTest.php tests/Feature/Workspaces/ManagedTenantOnboardingProviderStartTest.php tests/Feature/Restore/RestoreRunProviderStartTest.php tests/Feature/Directory/ProviderBackedDirectoryStartTest.php tests/Feature/Onboarding/OnboardingRbacSemanticsTest.php tests/Feature/Filament/RestoreRunUiEnforcementTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Operations/ProviderBackedRunReasonAlignmentTest.php tests/Feature/OpsUx/CanonicalViewRunLinksTest.php tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php tests/Feature/OpsUx/Constitution/JobDbNotificationGuardTest.php tests/Feature/OpsUx/NoQueuedDbNotificationsTest.php tests/Feature/Guards/ProviderDispatchGateCoverageTest.php tests/Feature/Guards/TestLaneManifestTest.php
  • Fixture / helper / factory / seed / context cost risks: Provider-backed start fixtures need tenant membership, explicit provider connections, active-run setup, and onboarding draft state; these remain explicit per test and must not become default helpers.
  • Expensive defaults or shared helper growth introduced?: No. Existing factories and helpers remain opt-in; no global provider/workspace bootstrap is planned.
  • Heavy-family additions, promotions, or visibility changes: None
  • Surface-class relief / special coverage rule: Standard native Filament relief for resource/page action hosts; onboarding uses a named wizard-step profile but still remains feature-testable without browser automation.
  • Closing validation and reviewer handoff: Re-run pint, then the focused test command above, then do one human smoke pass for blocked, deduped, scope busy, and queued outcomes across tenant, provider-connection, onboarding, and restore. Reviewers should confirm there is no remaining direct-dispatch bypass on a route-bounded covered surface, that scheduled or system-run Monitoring wording stays compatible with the same problem classes, and that no new custom completion notification path was introduced.
  • Budget / baseline / trend follow-up: None expected; document in feature if the new focused suite materially expands runtime beyond ordinary feature-local upkeep.
  • Review-stop questions: Did any coverage drift into browser/heavy lanes without a unique proving need? Did any helper start providing implicit tenant/provider context? Did any route-bounded covered start remain on local ensureRun*/dispatch logic? Did scheduled or system-run compatibility widen into click-time UX scope? Did any test assert presentation details that should stay in the shared presenter seam only?
  • Escalation path: document-in-feature
  • Active feature PR close-out entry: Guardrail
  • Why no dedicated follow-up spec is needed: This change is feature-local hardening around existing start surfaces. Only recurring operation-type normalization or structural lane-cost drift would justify a follow-up spec.

Project Structure

Documentation (this feature)

specs/216-provider-dispatch-gate/
├── plan.md
├── research.md
├── data-model.md
├── quickstart.md
├── contracts/
│   └── provider-dispatch-gate.logical.openapi.yaml
└── tasks.md

Source Code (repository root)

apps/platform/
├── app/
│   ├── Filament/
│   │   ├── Pages/Operations/TenantlessOperationRunViewer.php
│   │   ├── Pages/Workspaces/ManagedTenantOnboardingWizard.php
│   │   ├── Resources/EntraGroupResource/Pages/ListEntraGroups.php
│   │   ├── Resources/OperationRunResource.php
│   │   ├── Resources/ProviderConnectionResource.php
│   │   ├── Resources/RestoreRunResource.php
│   │   └── Resources/TenantResource.php
│   ├── Notifications/
│   │   └── OperationRunCompleted.php
│   ├── Services/
│   │   ├── Directory/RoleDefinitionsSyncService.php
│   │   ├── Providers/ProviderConnectionResolver.php
│   │   ├── Providers/ProviderOperationRegistry.php
│   │   ├── Providers/ProviderOperationStartGate.php
│   │   └── Verification/StartVerification.php
│   └── Support/
│       ├── OperationRunLinks.php
│       ├── OpsUx/OperationUxPresenter.php
│       ├── OpsUx/ProviderOperationStartResultPresenter.php
│       ├── ReasonTranslation/
│       └── Providers/
└── tests/
    ├── Feature/
    │   ├── Directory/
   │   ├── Filament/
   │   ├── Guards/
   │   ├── Onboarding/
    │   ├── Operations/
   │   ├── OpsUx/
    │   ├── ProviderConnections/
    │   ├── Restore/
    │   ├── Tenants/
    │   └── Workspaces/
   ├── Support/
    └── Unit/Providers/

Structure Decision: Single Laravel application inside the monorepo. All runtime work lands in apps/platform, while planning artifacts stay under specs/216-provider-dispatch-gate.

Complexity Tracking

No constitutional violation is planned. One bounded addition is tracked explicitly because it adds a thin presentation layer.

Violation Why Needed Simpler Alternative Rejected Because
BLOAT-001 bounded presenter helper Five-plus real start surfaces already duplicate blocked/deduped/scope busy/accepted notification branches; one shared helper is the narrowest way to keep one operator vocabulary without inventing a framework Keeping page-local branches would fail FR-216-008/009, preserve inconsistent copy drift, and force every new covered surface to duplicate the same start contract again

Proportionality Review

  • Current operator problem: Operators can trigger provider-backed actions that look accepted at click time but only fail later because provider connection, consent, credential, or protected-scope prerequisites were missing. The same blocker also renders differently across tenant, provider-connection, onboarding, restore, and directory-sync surfaces.
  • Existing structure is insufficient because: The canonical gate already exists, but only a subset of surfaces use it and those surfaces still duplicate result rendering locally. Legacy ensureRun*/dispatch starts also resolve provider identity too late, which allows runtime default drift and inconsistent click-time feedback.
  • Narrowest correct implementation: Extend the existing ProviderOperationRegistry and ProviderOperationStartGate, add one thin start-result presentation helper over the current Ops UX primitives, and migrate the first-slice action hosts. Keep blocked/accepted truth in existing OperationRun records and do not add new persistence.
  • Ownership cost created: One small shared presentation helper, a broader but still focused feature test surface, and explicit registry entries for each newly covered operation type.
  • Alternative intentionally rejected: A new provider-start coordinator/orchestration framework or a second queue-admission path. Those would duplicate existing gate behavior, broaden the slice into architecture work, and violate FR-216-014.
  • Release truth: Current-release truth. The problem exists today on current operator-facing start surfaces.

Phase 0 Research Summary

  • Reuse the current gate and registry; adoption breadth is the gap, not missing queue-admission infrastructure.
  • Resolve and pin provider_connection_id at dispatch time for every accepted covered start to prevent runtime default drift.
  • Preserve blocked preflight truth as canonical blocked start state, but never admit background work for blocked starts.
  • Add one shared presentation helper over OperationUxPresenter, ReasonPresenter, ProviderNextStepsRegistry, and OperationRunLinks rather than leaving local Notification::make() branches in place.
  • Keep the operator-facing start vocabulary explicit: the canonical outcomes remain accepted, deduped, scope busy, and blocked, while public wording stays queued, already running, scope busy, and blocked.
  • Keep the first implementation slice bounded to tenant, provider-connection, onboarding, restore, and directory start surfaces named by the spec; workspace-level baseline/evidence/review generators remain outside this route-bounded slice.
  • Do not combine dispatch-gate hardening with operation-type normalization; legacy write-time operation strings stay as-is in this feature.
  • Keep scheduled and system-triggered compatibility bounded to Monitoring-side reason reuse and initiator-aware notification behavior; no click-time UX is added for those runs.
  • Normalize onboarding bootstrap from multi-start batch admission to sequential protected-scope admission without adding a new orchestration framework.

Phase 1 Design Summary

  • data-model.md documents the feature as a service-owned extension over existing ProviderConnection, OperationRun, and onboarding draft state plus new logical models for protected scope, preflight result, accepted run context, and shared presentation output.
  • contracts/provider-dispatch-gate.logical.openapi.yaml defines the internal action-start contract for tenant, provider-connection, restore, directory-sync, and onboarding action hosts plus canonical run-detail output.
  • quickstart.md defines the focused verification path for blocked, deduped, scope busy, and accepted results, including the onboarding bootstrap sequentialization case.

Implementation Strategy

  1. Extend canonical gate coverage

    • Expand ProviderOperationRegistry to include every first-slice covered operation type.
    • Keep the existing ProviderOperationStartGate as the single queue-admission path.
    • Ensure each migrated start resolves provider access before dispatch and passes the explicit provider_connection_id into both OperationRun context and job args.
  2. Centralize start-result presentation

    • Add one shared helper that consumes ProviderOperationStartResult and emits the standardized operator feedback mapping: accepted -> queued, deduped -> already running, scope busy -> scope busy, and blocked -> blocked.
    • Reuse OperationUxPresenter for queued and already running toast semantics and ReasonPresenter plus next-step metadata for blocked and scope busy outcomes.
    • Keep domain verbs and target nouns local to each action host while unifying outcome vocabulary and run-link behavior.
  3. Migrate already-gated surfaces first

    • Refactor StartVerification, TenantResource, TenantVerificationReport, ProviderConnectionResource, and the onboarding verification step to call the shared presentation helper instead of keeping local result-branching code.
    • Preserve current protection while removing duplicated operator copy paths.
  4. Migrate legacy direct-dispatch starts

    • Replace local ensureRun*/dispatch logic on restore execute, directory groups sync, role definitions sync, and onboarding bootstrap with the canonical gate.
    • Keep destructive restore semantics intact: existing preview, warning, confirmation, and authorization remain unchanged; only queue admission and feedback unify.
  5. Normalize onboarding bootstrap sequencing

    • Convert bootstrap admission from “start every selected provider-backed operation immediately” to “accept one protected-scope provider-backed operation at a time.”
    • Reuse existing onboarding draft state to retain pending bootstrap selections and run references instead of introducing an umbrella batch entity or new orchestration model.
  6. Align accepted-run diagnostics

    • Ensure canonical run detail and terminal notification reason translation stay aligned with the shared start-result problem classes whenever they describe the same accepted operation.
    • Keep scheduled and system-triggered Monitoring reason reuse in the same public language family without adding click-time start UX or breaking initiator-only notification policy.
    • Keep blocked starts distinguishable from accepted execution failures in monitoring and audit surfaces.
  7. Backfill proof and regression guards

    • Extend the gate unit suite for newly registered operation types and onboarding sequencing.
    • Add one route-bounded guard that inventories covered start hosts and fails if any first-slice action still bypasses ProviderOperationStartGate through local ensureRun*/dispatch logic.
    • Add focused feature coverage around each migrated action host and around cross-surface reason vocabulary alignment.

Risks and Mitigations

  • Onboarding bootstrap behavior change: The current wizard can admit more than one provider-backed run for one connection. The mitigation is to normalize the flow to one accepted protected-scope run per submission and keep the rest as pending follow-up using existing draft state.
  • Legacy operation-type aliases: Directory sync operations currently use legacy write-time names. The mitigation is to keep those names stable in this feature and avoid widening the scope into operation taxonomy cleanup.
  • Restore flow regression risk: Restore is destructive-like and already confirmation-heavy. The mitigation is to change only the start path and feedback contract, not the preview/confirmation/authorization model.
  • Hidden direct-dispatch bypasses: Additional provider-backed starts may exist outside obvious action hosts. The mitigation is to treat any remaining first-slice ensureRun*/dispatch path as a review blocker and keep the route-bounded scope explicit.
  • Over-testing via expensive fixtures: Provider-backed starts need tenant, provider, and active-run context. The mitigation is to keep those fixtures opt-in and avoid a new shared “full provider world” helper.

Post-Design Re-check

Phase 0 and Phase 1 outputs resolve the earlier design unknowns without introducing new gates, new persisted truth, or a second UI semantics framework. The plan remains constitution-compliant, bounded to current-release operator pain, and ready for /speckit.tasks.

Implementation Close-Out

  • Final lane outcome: Focused verification completed with pint --dirty --format agent, the documented quickstart suite, the broader migrated-surface regression suite, and the route-bounded guard/manifest close-out suite all passing during implementation.
  • Route-bounded covered-surface audit: tests/Feature/Guards/ProviderDispatchGateCoverageTest.php now guards the first-slice route-bounded provider-backed starts so tenant verification, provider-connection actions, restore execute, directory sync, role definitions sync, and onboarding bootstrap stay on canonical gate-owned entry points rather than regressing to local ensureRun / dispatchOrFail admission paths.
  • Guardrail close-out: Livewire v4.0+ compliance is preserved, provider registration remains in bootstrap/providers.php, no new global-search behavior was introduced, destructive restore semantics remain on existing confirmation/authorization paths, and no new Filament assets were added beyond the standing php artisan filament:assets deployment expectation when registered assets change.
  • Test governance disposition: document-in-feature remains the explicit disposition. The proof stays in unit plus focused feature lanes, with one new heavy-governance guard family (provider-dispatch-gate-coverage) recorded in the manifest instead of widening into browser or new heavy workflow suites.