# 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 connection’s run. Rejected because it can hide recent verification attempts started against a different (now selected) connection. ## Decision 5: Canonical tenantless run links are mandatory - 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.