# 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.

