TenantAtlas/specs/084-verification-surfaces-unification/research.md

4.4 KiB
Raw Blame History

Research — Verification Surfaces Unification (Spec 084)

Date: 2026-02-09

Decision 1: Unify on provider.connection.check OperationRun type

  • Decision: Use the existing OperationRun.type = provider.connection.check as the single verification run type for both:
    • Tenant detail “Verify configuration”
    • Onboarding “Verify access”
  • Rationale:
    • This run type already exists and is used by onboarding (ManagedTenantOnboardingWizard::startVerification()).
    • The job (ProviderConnectionHealthCheckJob) already produces a schema-valid verification report via VerificationReportWriter::write(...).
    • Dedupe and “scope busy” semantics are already implemented in ProviderOperationStartGate.
  • Alternatives considered:
    • Create a new run type (e.g., tenant.verification). Rejected because it would duplicate existing job logic and complicate dedupe and viewer behavior.

Decision 2: Tenant verification start uses StartVerification / ProviderOperationStartGate (enqueue-only)

  • Decision: Replace the tenant detail synchronous verification (TenantResource::verifyTenant()) with an enqueue-only start that:
    1. authorizes,
    2. creates/dedupes an OperationRun,
    3. dispatches ProviderConnectionHealthCheckJob,
    4. returns a canonical “View run” link.
  • Rationale:
    • Constitution requires external calls be observable and performed asynchronously via OperationRun.
    • The current tenant action performs Graph calls inline; onboarding already uses the queued run model.
    • Unifies UX and operational auditability.
  • Alternatives considered:
    • Keep tenant verification synchronous and only add a “view last run” viewer. Rejected because it preserves inconsistency and violates run observability for remote calls.

Decision 3: Completed blocked verification runs MUST always have a schema-valid stub report

  • Decision: When a verification run is finalized as blocked (outcome blocked) for provider.connection.check, immediately write a stub context.verification_report using VerificationReportWriter.
  • Rationale:
    • Both verification viewers render DB-only and expect a report for completed runs.
    • OperationRunService::finalizeBlockedRun() currently sets context.reason_code and context.next_steps but does not write a report, which produces a “report unavailable” state.
    • A stub report can encode the reason code and next steps in a consistent, schema-valid format.
  • Alternatives considered:
    • Modify VerificationReportViewer to fabricate a report at render time if blocked. Rejected because rendering must be DB-only and deterministic, and should not create derived data in the UI layer.
    • Add report writing inside OperationRunService::finalizeBlockedRun() for all operations. Rejected because not all blocked operations are “verification” and we should not inject verification reports into unrelated runs.

Decision 4: Embedded tenant viewer selects latest run attempt for tenant + type

  • Decision: In the tenant view, select the latest OperationRun attempt by:
    • tenant_id = current tenant,
    • type = provider.connection.check,
    • ordered by id desc.
  • Rationale:
    • Matches the clarified spec requirement: latest attempt even if queued/running.
    • Avoids coupling selection to provider connection id.
  • Alternatives considered:
    • Select by context.provider_connection_id and only show the default connections run. Rejected because it can hide recent verification attempts started against a different (now selected) connection.
  • Decision: All “View run” CTAs use OperationRunLinks::tenantlessView($runId) (route admin.operations.view).
  • Rationale:
    • Canonical URL improves supportability and reduces ambiguity.
    • Tenantless views must still enforce workspace + tenant entitlement (404 if missing).
  • Alternatives considered:
    • Use tenant-scoped run URLs for tenant pages. Rejected because canonical linking is a core requirement.

Decision 6: Authorization semantics follow RBAC-UX 404/403 split

  • Decision:
    • Non-members (missing workspace membership or tenant entitlement): deny-as-not-found (404) for tenant routes and tenantless operation views of tenant-associated runs.
    • Members without capability: show action visible-but-disabled (UX), but server enforces 403 on attempt.
  • Rationale:
    • Matches constitution RBAC-UX-002 and RBAC-UX-003.