openapi: 3.1.0
info:
  title: Global Context Shell Logical Contract
  version: 0.1.0
  summary: Logical HTTP contract for workspace and tenant shell context resolution and mutation flows
  description: >-
    This is a logical contract for Spec 199. The real routes render HTML and redirects,
    but the schemas below define the canonical request-scoped shell context and the
    expected redirect or recovery outcomes for shared workspace and tenant shell flows.

servers:
  - url: /
    description: Application root for admin and tenant shell entry surfaces

tags:
  - name: shell-context
  - name: workspace-switch
  - name: tenant-select
  - name: tenant-clear

paths:
  /admin:
    get:
      tags: [shell-context]
      summary: Resolve workspace-scoped shell entry
      description: Resolve the active workspace and optional tenant context for a workspace-scoped admin route, including query-backed tenant hints only where the contract explicitly allows them.
      parameters:
        - name: tenant
          in: query
          required: false
          schema:
            type: string
          description: Optional tenant external-ID hint on routes that explicitly allow query-backed shell resolution.
        - name: tenant_id
          in: query
          required: false
          schema:
            oneOf:
              - type: integer
              - type: string
          description: Optional tenant identifier hint on workspace-scoped routes that explicitly allow query-backed context hints.
      responses:
        '200':
          description: Workspace-scoped shell entry resolved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedShellContextEnvelope'
              examples:
                tenantless:
                  value:
                    resolvedContext:
                      state: tenantless_workspace
                      displayMode: tenantless
                      pageCategory: workspace_scoped
                      workspaceSource: session_workspace
                      tenantSource: none
                      workspace:
                        id: 42
                        slug: alpha-workspace
                        name: Alpha Workspace
                      tenant: null
                      recoveryDirective:
                        action: none
                        reason: null
                        destination: null
                        preserveIntendedUrl: false
                rememberedTenant:
                  value:
                    resolvedContext:
                      state: tenant_scoped
                      displayMode: tenant_scoped
                      pageCategory: workspace_scoped
                      workspaceSource: session_workspace
                      tenantSource: remembered
                      workspace:
                        id: 42
                        slug: alpha-workspace
                        name: Alpha Workspace
                      tenant:
                        id: 7
                        externalId: tenant-7
                        name: Tenant Seven
                      recoveryDirective:
                        action: none
                        reason: null
                        destination: null
                        preserveIntendedUrl: false
                queryHintTenant:
                  value:
                    resolvedContext:
                      state: tenant_scoped
                      displayMode: tenant_scoped
                      pageCategory: workspace_scoped
                      workspaceSource: session_workspace
                      tenantSource: query_hint
                      workspace:
                        id: 42
                        slug: alpha-workspace
                        name: Alpha Workspace
                      tenant:
                        id: 7
                        externalId: tenant-7
                        name: Tenant Seven
                      requestedContext:
                        workspaceIdentifier: null
                        tenantIdentifier: tenant-7
                        source: query_hint
                        pageCategory: workspace_scoped
                      recoveryDirective:
                        action: none
                        reason: null
                        destination: null
                        preserveIntendedUrl: false
        '302':
          description: No valid workspace could be resolved and the user must be redirected to a chooser or safe fallback.
        '404':
          description: The requested context implies inaccessible or invalid workspace-bound data that cannot be widened safely.

  /admin/choose-workspace:
    get:
      tags: [shell-context]
      summary: Resolve the explicit workspace chooser exception route
      description: Render the explicit workspace chooser exception route used when no workspace truth can be recovered or when the operator must select a workspace directly.
      responses:
        '200':
          description: Workspace chooser rendered as the explicit recovery route.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedShellContextEnvelope'
              examples:
                chooser:
                  value:
                    resolvedContext:
                      state: missing_workspace
                      displayMode: recovery
                      pageCategory: workspace_chooser_exception
                      workspaceSource: none
                      tenantSource: none
                      workspace: null
                      tenant: null
                      recoveryDirective:
                        action: none
                        reason: missing_workspace
                        destination: /admin/choose-workspace
                        preserveIntendedUrl: true

  /admin/choose-tenant:
    get:
      tags: [shell-context]
      summary: Resolve the explicit choose-tenant route after workspace selection
      description: Render the explicit choose-tenant route used when a resolved workspace has multiple selectable tenants.
      responses:
        '200':
          description: Choose-tenant route rendered successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedShellContextEnvelope'
              examples:
                chooseTenant:
                  value:
                    resolvedContext:
                      state: tenantless_workspace
                      displayMode: tenantless
                      pageCategory: workspace_scoped
                      workspaceSource: session_workspace
                      tenantSource: none
                      workspace:
                        id: 42
                        slug: alpha-workspace
                        name: Alpha Workspace
                      tenant: null
                      recoveryDirective:
                        action: none
                        reason: null
                        destination: /admin/choose-tenant
                        preserveIntendedUrl: false

  /admin/t/{external_id}:
    get:
      tags: [shell-context]
      summary: Resolve tenant-bound shell entry
      description: Resolve tenant context for a tenant-bound route where explicit tenant routing is required.
      parameters:
        - name: external_id
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Tenant-bound shell entry resolved successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResolvedShellContextEnvelope'
              examples:
                tenantBound:
                  value:
                    resolvedContext:
                      state: tenant_scoped
                      displayMode: tenant_scoped
                      pageCategory: tenant_bound
                      workspaceSource: session_workspace
                      tenantSource: route
                      workspace:
                        id: 42
                        slug: alpha-workspace
                        name: Alpha Workspace
                      tenant:
                        id: 7
                        externalId: tenant-7
                        name: Tenant Seven
                      recoveryDirective:
                        action: none
                        reason: null
                        destination: null
                        preserveIntendedUrl: false
        '404':
          description: The route tenant is invalid, inaccessible, or incompatible with the active workspace.

  /admin/switch-workspace:
    post:
      tags: [workspace-switch]
      summary: Switch the active workspace
      description: Set the active workspace, re-evaluate tenant compatibility, and redirect to a safe concrete destination such as an intended `/admin...` URL, `admin.workspace.managed-tenants.index`, `/admin/choose-tenant`, or the tenant dashboard.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [workspace_id]
              properties:
                workspace_id:
                  type: integer
      responses:
        '302':
          description: Workspace switch accepted and redirected to intended URL or resolver destination.
          headers:
            Location:
              schema:
                type: string
              description: Safe destination for the resolved workspace and resulting tenant state, currently an intended `/admin...` URL, `admin.workspace.managed-tenants.index`, `/admin/choose-tenant`, or a tenant dashboard route.
        '404':
          description: Workspace does not exist, is archived, or is not accessible to the current user.
        '422':
          description: Request body failed validation.

  /admin/select-tenant:
    post:
      tags: [tenant-select]
      summary: Select the active tenant inside the resolved workspace
      description: Explicitly activate a tenant that belongs to the current workspace and passes entitlement and operability checks, then redirect to the deterministic tenant entry route for that tenant.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [tenant_id]
              properties:
                tenant_id:
                  type: integer
      responses:
        '302':
          description: Tenant selection accepted and redirected to the deterministic tenant entry route for the selected tenant.
          headers:
            Location:
              schema:
                type: string
        '404':
          description: Tenant is missing, inaccessible, incompatible with the active workspace, or fails operability rules.
        '422':
          description: Request body failed validation.

  /admin/clear-tenant-context:
    post:
      tags: [tenant-clear]
      summary: Clear active tenant context
      description: Remove remembered and panel tenant state, then resolve according to page category and route compatibility to either same-route tenantless workspace state or one of the documented concrete destinations: `admin.operations.index`, `admin.evidence.overview`, `admin.workspace.managed-tenants.index`, `admin.operations.view`, or `admin.home`.
      responses:
        '302':
          description: Tenant context cleared and request resolved to tenantless workspace state on the current route or redirected to one of the documented concrete workspace-safe fallbacks.
          headers:
            Location:
              schema:
                type: string
        '404':
          description: The current route cannot recover safely because scope is no longer accessible.

components:
  schemas:
    RequestedContext:
      type: object
      properties:
        workspaceIdentifier:
          oneOf:
            - type: integer
            - type: string
            - type: 'null'
        tenantIdentifier:
          oneOf:
            - type: integer
            - type: string
            - type: 'null'
        source:
          $ref: '#/components/schemas/ContextSource'
        pageCategory:
          $ref: '#/components/schemas/PageCategory'

    RememberedContextCandidate:
      type: object
      properties:
        workspaceId:
          type: integer
        tenantId:
          type:
            - integer
            - 'null'
        source:
          $ref: '#/components/schemas/ContextSource'
        eligible:
          type: boolean
        invalidReason:
          type:
            - string
            - 'null'

    ResolvedShellContextEnvelope:
      type: object
      required: [resolvedContext]
      properties:
        resolvedContext:
          $ref: '#/components/schemas/ResolvedShellContext'

    ResolvedShellContext:
      type: object
      required:
        - state
        - displayMode
        - pageCategory
        - workspaceSource
        - tenantSource
        - workspace
        - tenant
        - recoveryDirective
      properties:
        state:
          $ref: '#/components/schemas/ShellState'
        displayMode:
          type: string
          enum:
            - tenant_scoped
            - tenantless
            - recovery
        pageCategory:
          $ref: '#/components/schemas/PageCategory'
        workspaceSource:
          $ref: '#/components/schemas/ContextSource'
        tenantSource:
          $ref: '#/components/schemas/ContextSource'
        workspace:
          oneOf:
            - $ref: '#/components/schemas/WorkspaceReference'
            - type: 'null'
        tenant:
          oneOf:
            - $ref: '#/components/schemas/TenantReference'
            - type: 'null'
        requestedContext:
          oneOf:
            - $ref: '#/components/schemas/RequestedContext'
            - type: 'null'
        rememberedContext:
          oneOf:
            - $ref: '#/components/schemas/RememberedContextCandidate'
            - type: 'null'
        recoveryDirective:
          $ref: '#/components/schemas/RecoveryDirective'

    RecoveryDirective:
      type: object
      required: [action, reason, destination, preserveIntendedUrl]
      properties:
        action:
          $ref: '#/components/schemas/RecoveryAction'
        reason:
          type:
            - string
            - 'null'
        destination:
          type:
            - string
            - 'null'
        preserveIntendedUrl:
          type: boolean

    WorkspaceReference:
      type: object
      required: [id, slug, name]
      properties:
        id:
          type: integer
        slug:
          type: string
        name:
          type: string

    TenantReference:
      type: object
      required: [id, externalId, name]
      properties:
        id:
          type: integer
        externalId:
          type: string
        name:
          type: string

    ContextSource:
      type: string
      enum:
        - route
        - explicit_switch
        - explicit_select
        - session_workspace
        - filament_tenant
        - remembered
        - query_hint
        - none

    PageCategory:
      type: string
      enum:
        - workspace_scoped
        - workspace_chooser_exception
        - tenant_bound
        - tenant_scoped_evidence
        - canonical_workspace_record_viewer

    ShellState:
      type: string
      enum:
        - tenant_scoped
        - tenantless_workspace
        - missing_workspace
        - invalid_workspace
        - missing_tenant
        - invalid_tenant
        - inaccessible_tenant
        - incompatible_tenant

    RecoveryAction:
      type: string
      enum:
        - none
        - render_tenantless_workspace
        - redirect_choose_workspace
        - redirect_operations_index
        - redirect_evidence_overview
        - redirect_workspace_home
        - redirect_workspace_managed_tenants
        - redirect_workspace_record_fallback
        - abort_not_found