openapi: 3.0.3
info:
  title: Dealspace API
  description: |
    M&A deal management platform API. Investment Banks, Sellers, and Buyers collaborate 
    on a structured request-and-answer workflow system.
    
    ## Authentication
    All endpoints except `/health` and `/api/auth/*` require a valid Bearer token.
    Tokens are JWT (HS256) with 1-hour access token lifetime and 7-day refresh token lifetime.
    
    ## RBAC Model
    Access is scoped to projects and workstreams. Roles determine permissions:
    - **ib_admin** — Full control over project and all workstreams
    - **ib_member** — Manage requests and vet answers in assigned workstreams
    - **seller_admin** — Manage seller team, see all seller-directed requests
    - **seller_member** — Answer requests in assigned workstreams
    - **buyer_admin** — Manage buyer team, access data room
    - **buyer_member** — Submit requests, view published answers
    - **observer** — Read-only access to assigned workstreams
    
    ## Error Responses
    All errors follow a consistent format:
    ```json
    {"error": "Human-readable message", "code": "ERROR_CODE", "details": {}}
    ```
    
    ## Rate Limiting
    Endpoints are rate-limited per user, IP, and project. Headers `X-RateLimit-Limit`, 
    `X-RateLimit-Remaining`, and `Retry-After` are included in responses.
  version: 1.0.0
  contact:
    name: Dealspace Support
    email: support@muskepo.com
  license:
    name: Proprietary
    
servers:
  - url: https://muskepo.com/api
    description: Production
  - url: http://localhost:8080/api
    description: Development

tags:
  - name: Auth
    description: Authentication and MFA
  - name: Projects
    description: Top-level deal containers
  - name: Workstreams
    description: RBAC-anchored workflow containers (Finance, Legal, IT, etc.)
  - name: Request Lists
    description: Named collections of requests within a workstream
  - name: Requests
    description: Individual request items
  - name: Answers
    description: Responses to requests
  - name: Matches
    description: AI-suggested answer-to-request matching
  - name: Objects
    description: File upload/download with watermarking
  - name: Tasks
    description: Personal task inbox
  - name: Access
    description: RBAC grants
  - name: Invites
    description: Project invitations
  - name: Audit
    description: Security audit log
  - name: Health
    description: Service health check

paths:
  # ============================================================================
  # AUTH ENDPOINTS
  # ============================================================================
  
  /auth/login:
    post:
      tags: [Auth]
      summary: Authenticate with email and password
      description: |
        Returns access and refresh tokens. If MFA is required but not yet verified,
        the token will have `mfa: false` and can only be used on `/auth/mfa/*` endpoints.
        
        **Rate limit:** 5 requests/minute per IP
      operationId: login
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email, password]
              properties:
                email:
                  type: string
                  format: email
                  example: user@example.com
                password:
                  type: string
                  format: password
                  minLength: 8
                fingerprint:
                  type: object
                  description: Device fingerprint for session binding
                  properties:
                    userAgent:
                      type: string
                    acceptLanguage:
                      type: string
                    timezone:
                      type: string
      responses:
        '200':
          description: Authentication successful
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '429':
          description: Rate limit exceeded
          headers:
            Retry-After:
              schema:
                type: integer
              description: Seconds until rate limit resets
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /auth/refresh:
    post:
      tags: [Auth]
      summary: Rotate refresh token
      description: |
        Exchange a valid refresh token for new access and refresh tokens.
        The old refresh token is invalidated (rotation). Fingerprint must match
        the original session.
        
        **Rate limit:** 60 requests/minute per user
      operationId: refreshToken
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [refresh_token]
              properties:
                refresh_token:
                  type: string
                fingerprint:
                  type: object
                  properties:
                    userAgent:
                      type: string
                    acceptLanguage:
                      type: string
                    timezone:
                      type: string
      responses:
        '200':
          description: Tokens rotated successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Invalid or expired refresh token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /auth/logout:
    post:
      tags: [Auth]
      summary: Revoke current session
      description: |
        Invalidates the current access token and all associated refresh tokens.
        
        **Rate limit:** 60 requests/minute per user
      operationId: logout
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Session revoked successfully
        '401':
          description: Invalid or expired token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /auth/mfa/setup:
    post:
      tags: [Auth]
      summary: Initiate TOTP MFA setup
      description: |
        Generates a new TOTP secret and returns a QR code URI for authenticator apps.
        Also returns recovery codes (store securely, shown only once).
        
        **RBAC:** Any authenticated user
        **Rate limit:** 5 requests/minute per user
      operationId: mfaSetup
      security:
        - bearerAuth: []
      responses:
        '200':
          description: MFA setup initiated
          content:
            application/json:
              schema:
                type: object
                properties:
                  secret:
                    type: string
                    description: Base32-encoded TOTP secret (for manual entry)
                  qr_uri:
                    type: string
                    format: uri
                    description: otpauth:// URI for QR code generation
                  recovery_codes:
                    type: array
                    items:
                      type: string
                    description: 10 single-use recovery codes
        '400':
          description: MFA already enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /auth/mfa/verify:
    post:
      tags: [Auth]
      summary: Verify TOTP code
      description: |
        Completes MFA setup (if pending) or verifies MFA during login.
        On successful verification, returns a new token with `mfa: true`.
        
        **Rate limit:** 5 requests/minute per user (prevents brute force)
      operationId: mfaVerify
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [code]
              properties:
                code:
                  type: string
                  pattern: '^\d{6}$'
                  description: 6-digit TOTP code from authenticator app
      responses:
        '200':
          description: MFA verified
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AuthTokens'
        '401':
          description: Invalid TOTP code
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /invites/accept:
    post:
      tags: [Auth]
      summary: Accept invitation and create account
      description: |
        Accepts an invite token. If the user does not exist, creates a new account.
        If the user exists (by email), grants access to the invited project/workstream.
        
        **Rate limit:** 10 requests/minute per IP
      operationId: acceptInvite
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [token]
              properties:
                token:
                  type: string
                  description: Invite token from email
                name:
                  type: string
                  description: User's full name (required if creating new account)
                password:
                  type: string
                  format: password
                  minLength: 8
                  description: Password (required if creating new account)
      responses:
        '200':
          description: Invitation accepted
          content:
            application/json:
              schema:
                type: object
                properties:
                  user_id:
                    type: string
                    format: uuid
                  project_id:
                    type: string
                    format: uuid
                  role:
                    type: string
                  tokens:
                    $ref: '#/components/schemas/AuthTokens'
        '400':
          description: Invalid, expired, or already-used invite
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  # ============================================================================
  # PROJECT ENDPOINTS
  # ============================================================================
  
  /projects:
    get:
      tags: [Projects]
      summary: List all projects for the authenticated user
      description: |
        Returns all projects the user has access to, based on their role grants.
        
        **RBAC:** Any authenticated user
        **Rate limit:** 300 requests/minute per user
      operationId: listProjects
      security:
        - bearerAuth: []
      parameters:
        - name: stage
          in: query
          schema:
            type: string
            enum: [pre_dataroom, dataroom, closed]
          description: Filter by deal stage
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  projects:
                    type: array
                    items:
                      $ref: '#/components/schemas/Project'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Projects]
      summary: Create a new project
      description: |
        Creates a new project. The creator automatically becomes `ib_admin`.
        
        **RBAC:** Any authenticated user (typically IB initiates deals)
        **Rate limit:** 10 requests/minute per user
      operationId: createProject
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectCreate'
      responses:
        '201':
          description: Project created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /projects/{id}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    get:
      tags: [Projects]
      summary: Get project details
      description: |
        Returns project metadata and summary statistics.
        
        **RBAC:** Any role with access to the project
        **Rate limit:** 300 requests/minute per user
      operationId: getProject
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Project details
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Projects]
      summary: Update project
      description: |
        Updates project metadata (name, stage, theme, etc.).
        
        **RBAC:** `ib_admin` only
        **Rate limit:** 60 requests/minute per user
      operationId: updateProject
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ProjectUpdate'
      responses:
        '200':
          description: Project updated
          headers:
            ETag:
              schema:
                type: string
              description: Updated version tag for concurrency control
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Project'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'

    delete:
      tags: [Projects]
      summary: Soft delete project
      description: |
        Marks the project as deleted. Data is retained for compliance but
        project becomes inaccessible to all users.
        
        **RBAC:** `ib_admin` only
        **Rate limit:** 10 requests/minute per user
      operationId: deleteProject
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Project deleted
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================================================
  # WORKSTREAM ENDPOINTS
  # ============================================================================
  
  /projects/{id}/workstreams:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    get:
      tags: [Workstreams]
      summary: List workstreams in project
      description: |
        Returns all workstreams the user has access to within the project.
        
        **RBAC:** Any role with project access (filtered by workstream-level access)
        **Rate limit:** 300 requests/minute per user
      operationId: listWorkstreams
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of workstreams
          content:
            application/json:
              schema:
                type: object
                properties:
                  workstreams:
                    type: array
                    items:
                      $ref: '#/components/schemas/Workstream'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Workstreams]
      summary: Create workstream
      description: |
        Creates a new workstream within the project. Workstreams are RBAC anchors.
        
        **RBAC:** `ib_admin` only
        **Rate limit:** 60 requests/minute per user
      operationId: createWorkstream
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkstreamCreate'
      responses:
        '201':
          description: Workstream created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workstream'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      
    patch:
      tags: [Workstreams]
      summary: Update workstream
      description: |
        Updates workstream metadata (name, description).
        
        **RBAC:** `ib_admin` or `ib_member` with access to this workstream
        **Rate limit:** 60 requests/minute per user
      operationId: updateWorkstream
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WorkstreamUpdate'
      responses:
        '200':
          description: Workstream updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Workstream'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================================================
  # REQUEST LIST ENDPOINTS
  # ============================================================================
  
  /projects/{id}/workstreams/{wsId}/lists:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      
    get:
      tags: [Request Lists]
      summary: List request lists in workstream
      description: |
        Returns all request lists within the workstream.
        
        **RBAC:** Any role with workstream access
        **Rate limit:** 300 requests/minute per user
      operationId: listRequestLists
      security:
        - bearerAuth: []
      responses:
        '200':
          description: List of request lists
          content:
            application/json:
              schema:
                type: object
                properties:
                  lists:
                    type: array
                    items:
                      $ref: '#/components/schemas/RequestList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Request Lists]
      summary: Create request list
      description: |
        Creates a new named collection of requests within the workstream.
        
        **RBAC:** `ib_admin`, `ib_member`
        **Rate limit:** 60 requests/minute per user
      operationId: createRequestList
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestListCreate'
      responses:
        '201':
          description: Request list created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/RequestList'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================================================
  # REQUEST ENDPOINTS
  # ============================================================================
  
  /projects/{id}/workstreams/{wsId}/requests:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      
    get:
      tags: [Requests]
      summary: List requests in workstream
      description: |
        Returns requests in the workstream. Supports filtering by assignee, status, and stage.
        
        **RBAC:** Any role with workstream access. Pre-dataroom entries invisible to buyers.
        **Rate limit:** 300 requests/minute per user
      operationId: listRequests
      security:
        - bearerAuth: []
      parameters:
        - name: assignee
          in: query
          schema:
            type: string
          description: Filter by assignee. Use "me" for current user.
        - name: status
          in: query
          schema:
            type: string
            enum: [open, assigned, answered, vetted, published, closed]
          description: Filter by status
        - name: stage
          in: query
          schema:
            type: string
            enum: [pre_dataroom, dataroom, closed]
          description: Filter by deal stage
        - name: list_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by request list
        - name: priority
          in: query
          schema:
            type: string
            enum: [high, normal, low]
        - name: overdue
          in: query
          schema:
            type: boolean
          description: Only return requests past due date
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of requests
          content:
            application/json:
              schema:
                type: object
                properties:
                  requests:
                    type: array
                    items:
                      $ref: '#/components/schemas/Request'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Requests]
      summary: Create request
      description: |
        Creates a new request in the workstream. IB/Seller create for seller action,
        Buyers create via data room (routed to IB).
        
        **RBAC:** `ib_admin`, `ib_member`, `buyer_admin`, `buyer_member`
        **Rate limit:** 60 requests/minute per user
      operationId: createRequest
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestCreate'
      responses:
        '201':
          description: Request created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Request'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/requests/{reqId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      
    get:
      tags: [Requests]
      summary: Get request details
      description: |
        Returns full request details including routing chain (visible to IB only).
        
        **RBAC:** Any role with workstream access
        **Rate limit:** 300 requests/minute per user
      operationId: getRequest
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Request details
          headers:
            ETag:
              schema:
                type: string
              description: Version tag for concurrency control
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Request'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Requests]
      summary: Update request
      description: |
        Updates request metadata (title, body, priority, due_date, assigned_to, status).
        Use `If-Match` header with ETag for optimistic concurrency control.
        
        **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member` (own assignments)
        **Rate limit:** 60 requests/minute per user
      operationId: updateRequest
      security:
        - bearerAuth: []
      parameters:
        - name: If-Match
          in: header
          schema:
            type: string
          description: ETag from previous GET for concurrency control
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RequestUpdate'
      responses:
        '200':
          description: Request updated
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Request'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'
        '412':
          $ref: '#/components/responses/PreconditionFailed'

  /projects/{id}/workstreams/{wsId}/requests/{reqId}/forward:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      
    post:
      tags: [Requests]
      summary: Forward request in routing chain
      description: |
        Forwards the request to another user. Creates a routing chain hop.
        The `return_to_id` is set automatically so completion returns to the forwarder.
        
        **RBAC:** Current assignee or `ib_admin`
        **Rate limit:** 60 requests/minute per user
      operationId: forwardRequest
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [to_user_id]
              properties:
                to_user_id:
                  type: string
                  format: uuid
                  description: User to forward the request to
                message:
                  type: string
                  description: Optional message for the recipient
      responses:
        '200':
          description: Request forwarded
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Request'
        '400':
          description: Invalid recipient
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/requests/{reqId}/complete:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      
    post:
      tags: [Requests]
      summary: Mark request as complete
      description: |
        Marks the request as completed by the current assignee. If `return_to_id` is set,
        the request is automatically routed back to that user's inbox. Otherwise, it moves
        to the final status.
        
        **RBAC:** Current assignee only
        **Rate limit:** 60 requests/minute per user
      operationId: completeRequest
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                message:
                  type: string
                  description: Optional completion note
      responses:
        '200':
          description: Request completed and routed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Request'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================================================
  # ANSWER ENDPOINTS
  # ============================================================================
  
  /projects/{id}/workstreams/{wsId}/answers:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      
    post:
      tags: [Answers]
      summary: Create an answer
      description: |
        Creates a new answer in the workstream. Answers can be linked to one or more
        requests. Initially created in `draft` status.
        
        **RBAC:** `seller_admin`, `seller_member`, `ib_admin`, `ib_member`
        **Rate limit:** 60 requests/minute per user
      operationId: createAnswer
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnswerCreate'
      responses:
        '201':
          description: Answer created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '400':
          description: Invalid input
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/answers/{ansId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/AnswerId'
      
    get:
      tags: [Answers]
      summary: Get answer details
      description: |
        Returns full answer details including linked requests and files.
        
        **RBAC:** Depends on answer status. Draft/submitted visible to IB+seller. 
        Published visible to all with workstream access.
        **Rate limit:** 300 requests/minute per user
      operationId: getAnswer
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Answer details
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    patch:
      tags: [Answers]
      summary: Update answer
      description: |
        Updates answer content. Only allowed in `draft` or `rejected` status.
        
        **RBAC:** Answer creator, `seller_admin`, or `ib_admin`
        **Rate limit:** 60 requests/minute per user
      operationId: updateAnswer
      security:
        - bearerAuth: []
      parameters:
        - name: If-Match
          in: header
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AnswerUpdate'
      responses:
        '200':
          description: Answer updated
          headers:
            ETag:
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '409':
          $ref: '#/components/responses/Conflict'

  /projects/{id}/workstreams/{wsId}/answers/{ansId}/submit:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/AnswerId'
      
    post:
      tags: [Answers]
      summary: Submit answer for vetting
      description: |
        Submits the answer for IB review. Transitions status from `draft` to `submitted`.
        
        **RBAC:** Answer creator, `seller_admin`
        **Rate limit:** 60 requests/minute per user
      operationId: submitAnswer
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Answer submitted for vetting
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '400':
          description: Answer not in draft status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/answers/{ansId}/approve:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/AnswerId'
      
    post:
      tags: [Answers]
      summary: Approve answer
      description: |
        IB approves the answer. Transitions status from `submitted` to `approved`.
        Answer is now ready for publishing to data room.
        
        **RBAC:** `ib_admin`, `ib_member` with workstream access
        **Rate limit:** 60 requests/minute per user
      operationId: approveAnswer
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Answer approved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '400':
          description: Answer not in submitted status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/answers/{ansId}/reject:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/AnswerId'
      
    post:
      tags: [Answers]
      summary: Reject answer
      description: |
        IB rejects the answer with a reason. Transitions status from `submitted` to `rejected`.
        Seller can then edit and resubmit.
        
        **RBAC:** `ib_admin`, `ib_member` with workstream access
        **Rate limit:** 60 requests/minute per user
      operationId: rejectAnswer
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [reason]
              properties:
                reason:
                  type: string
                  minLength: 1
                  description: Explanation for why the answer was rejected
      responses:
        '200':
          description: Answer rejected
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '400':
          description: Answer not in submitted status or missing reason
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/answers/{ansId}/publish:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/AnswerId'
      
    post:
      tags: [Answers]
      summary: Publish answer to data room
      description: |
        Publishes an approved answer to the data room. All linked requests are updated
        and broadcast notifications sent to all requesters who asked equivalent questions.
        
        **RBAC:** `ib_admin`, `ib_member` with workstream access
        **Rate limit:** 60 requests/minute per user
      operationId: publishAnswer
      security:
        - bearerAuth: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                broadcast_to:
                  type: string
                  enum: [linked_requesters, all_workstream, all_dataroom]
                  default: linked_requesters
                  description: |
                    Broadcast scope:
                    - `linked_requesters`: Only users who submitted linked requests
                    - `all_workstream`: All users with workstream access
                    - `all_dataroom`: All users with any data room access
      responses:
        '200':
          description: Answer published
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Answer'
        '400':
          description: Answer not in approved status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================================================
  # AI MATCHING ENDPOINTS
  # ============================================================================
  
  /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      
    get:
      tags: [Matches]
      summary: Get AI-suggested answer matches
      description: |
        Returns published answers that the AI has identified as potentially satisfying
        this request (cosine similarity >= 0.72). Human confirmation is required before
        linking.
        
        **RBAC:** `ib_admin`, `ib_member`
        **Rate limit:** 20 requests/minute per user (embedding cost control)
      operationId: getMatches
      security:
        - bearerAuth: []
      parameters:
        - name: min_score
          in: query
          schema:
            type: number
            minimum: 0
            maximum: 1
            default: 0.72
          description: Minimum similarity score threshold
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
            maximum: 25
      responses:
        '200':
          description: List of suggested matches
          content:
            application/json:
              schema:
                type: object
                properties:
                  matches:
                    type: array
                    items:
                      $ref: '#/components/schemas/AnswerMatch'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/confirm:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      - $ref: '#/components/parameters/AnswerIdPath'
      
    post:
      tags: [Matches]
      summary: Confirm AI match
      description: |
        Human confirms that the suggested answer satisfies this request.
        Creates a confirmed link in `answer_links` and updates request status.
        Triggers broadcast to the requester.
        
        **RBAC:** `ib_admin`, `ib_member`
        **Rate limit:** 60 requests/minute per user
      operationId: confirmMatch
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match confirmed
          content:
            application/json:
              schema:
                type: object
                properties:
                  request:
                    $ref: '#/components/schemas/Request'
                  answer:
                    $ref: '#/components/schemas/Answer'
                  link_id:
                    type: string
                    format: uuid
        '400':
          description: Answer not published or already linked
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/reject:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/WorkstreamId'
      - $ref: '#/components/parameters/RequestId'
      - $ref: '#/components/parameters/AnswerIdPath'
      
    post:
      tags: [Matches]
      summary: Reject AI match
      description: |
        Human rejects the AI suggestion. The answer will not be suggested again for
        this request (stored in `answer_links` with rejected flag).
        
        **RBAC:** `ib_admin`, `ib_member`
        **Rate limit:** 60 requests/minute per user
      operationId: rejectMatch
      security:
        - bearerAuth: []
      responses:
        '200':
          description: Match rejected
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                    example: Match rejected and will not be suggested again
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================================================
  # FILE/OBJECT ENDPOINTS
  # ============================================================================
  
  /projects/{id}/objects:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    post:
      tags: [Objects]
      summary: Upload file
      description: |
        Uploads a file to the project's object store. Files are encrypted at rest.
        Returns an objectId that can be referenced in answers.
        
        **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member`
        **Rate limit:** 10 uploads/minute per user
        **Max size:** 100MB per file
      operationId: uploadObject
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              required: [file]
              properties:
                file:
                  type: string
                  format: binary
                filename:
                  type: string
                  description: Override filename (defaults to uploaded name)
                description:
                  type: string
                  description: File description for search
      responses:
        '201':
          description: File uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  object_id:
                    type: string
                    pattern: '^[a-f0-9]{16}$'
                    description: Content-addressable object ID
                  filename:
                    type: string
                  size:
                    type: integer
                    description: File size in bytes
                  mime_type:
                    type: string
                  uploaded_at:
                    type: string
                    format: date-time
        '400':
          description: Invalid file or missing data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '413':
          description: File too large
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

  /projects/{id}/objects/{objectId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/ObjectId'
      
    get:
      tags: [Objects]
      summary: Download file with watermark
      description: |
        Downloads a file with dynamic watermark applied. Watermarks include:
        - User name and organization
        - Timestamp
        - CONFIDENTIAL marking
        
        PDF, Word, Excel, and images have visible watermarks.
        Other files are encrypted downloads only.
        
        **RBAC:** Any role with access to the answer containing this file
        **Rate limit:** 50 downloads/minute per user (exfiltration protection)
      operationId: downloadObject
      security:
        - bearerAuth: []
      parameters:
        - name: preview
          in: query
          schema:
            type: boolean
            default: false
          description: Return preview (lower quality) instead of full file
      responses:
        '200':
          description: File content with watermark
          headers:
            Content-Disposition:
              schema:
                type: string
              example: 'attachment; filename="document.pdf"'
            Content-Type:
              schema:
                type: string
            X-Watermark-Applied:
              schema:
                type: string
                enum: [visible, encrypted, none]
          content:
            '*/*':
              schema:
                type: string
                format: binary
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

    delete:
      tags: [Objects]
      summary: Delete file
      description: |
        Deletes a file from the object store. File must not be referenced by any
        published answer.
        
        **RBAC:** File uploader, `ib_admin`, `seller_admin`
        **Rate limit:** 60 requests/minute per user
      operationId: deleteObject
      security:
        - bearerAuth: []
      responses:
        '204':
          description: File deleted
        '400':
          description: File is referenced by published answer
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================================================
  # TASK ENDPOINTS
  # ============================================================================
  
  /tasks:
    get:
      tags: [Tasks]
      summary: List personal tasks
      description: |
        Returns all tasks (requests) assigned to the current user across all projects.
        This is the user's personal inbox.
        
        **RBAC:** Any authenticated user
        **Rate limit:** 300 requests/minute per user
      operationId: listTasks
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [open, assigned, answered, vetted, published, closed]
          description: Filter by status
        - name: overdue
          in: query
          schema:
            type: boolean
          description: Only return tasks past due date
        - name: project_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by project
        - name: priority
          in: query
          schema:
            type: string
            enum: [high, normal, low]
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 100
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: List of tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  tasks:
                    type: array
                    items:
                      $ref: '#/components/schemas/Task'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================================
  # ACCESS ENDPOINTS
  # ============================================================================
  
  /projects/{id}/access:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    get:
      tags: [Access]
      summary: List access grants
      description: |
        Returns all access grants for the project.
        
        **RBAC:** `ib_admin`, `seller_admin` (own org), `buyer_admin` (own org)
        **Rate limit:** 300 requests/minute per user
      operationId: listAccess
      security:
        - bearerAuth: []
      parameters:
        - name: role
          in: query
          schema:
            type: string
            enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
          description: Filter by role
        - name: workstream_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by workstream
      responses:
        '200':
          description: List of access grants
          content:
            application/json:
              schema:
                type: object
                properties:
                  grants:
                    type: array
                    items:
                      $ref: '#/components/schemas/AccessGrant'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Access]
      summary: Grant access
      description: |
        Grants a role to a user for the project and/or specific workstream.
        Users can only grant roles at or below their level.
        IB can grant any role. Seller/Buyer can only grant within their org type.
        
        **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with hierarchy constraints)
        **Rate limit:** 60 requests/minute per user
      operationId: grantAccess
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AccessGrantCreate'
      responses:
        '201':
          description: Access granted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AccessGrant'
        '400':
          description: Invalid input or role hierarchy violation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/access/{grantId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/GrantId'
      
    delete:
      tags: [Access]
      summary: Revoke access
      description: |
        Revokes an access grant. All active sessions for the affected user are immediately
        invalidated for this project.
        
        **RBAC:** `ib_admin`, or admin of same org type who granted the access
        **Rate limit:** 60 requests/minute per user
      operationId: revokeAccess
      security:
        - bearerAuth: []
      parameters:
        - name: reason
          in: query
          schema:
            type: string
          description: Audit trail reason for revocation
      responses:
        '204':
          description: Access revoked
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================================================
  # INVITE ENDPOINTS
  # ============================================================================
  
  /projects/{id}/invites:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    get:
      tags: [Invites]
      summary: List pending invites
      description: |
        Returns all pending invitations for the project.
        
        **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin`
        **Rate limit:** 300 requests/minute per user
      operationId: listInvites
      security:
        - bearerAuth: []
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [pending, accepted, expired, revoked]
      responses:
        '200':
          description: List of invites
          content:
            application/json:
              schema:
                type: object
                properties:
                  invites:
                    type: array
                    items:
                      $ref: '#/components/schemas/Invite'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

    post:
      tags: [Invites]
      summary: Create invite
      description: |
        Creates an invitation for a user to join the project with a specific role.
        Invites are scoped to a project and optionally to specific workstreams.
        Invite token expires in 72 hours.
        
        **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with role hierarchy constraints)
        **Rate limit:** 30 requests/minute per user
      operationId: createInvite
      security:
        - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/InviteCreate'
      responses:
        '201':
          description: Invite created and sent
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Invite'
        '400':
          description: Invalid input or role hierarchy violation
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  /projects/{id}/invites/{inviteId}:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      - $ref: '#/components/parameters/InviteId'
      
    delete:
      tags: [Invites]
      summary: Revoke invite
      description: |
        Revokes a pending invitation. If already accepted, use access revocation instead.
        
        **RBAC:** Invite creator or `ib_admin`
        **Rate limit:** 60 requests/minute per user
      operationId: revokeInvite
      security:
        - bearerAuth: []
      responses:
        '204':
          description: Invite revoked
        '400':
          description: Invite already accepted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'

  # ============================================================================
  # AUDIT ENDPOINT
  # ============================================================================
  
  /projects/{id}/audit:
    parameters:
      - $ref: '#/components/parameters/ProjectId'
      
    get:
      tags: [Audit]
      summary: Get audit log
      description: |
        Returns paginated audit log entries for the project. Includes all security-relevant
        events: access changes, file downloads, status transitions, logins.
        
        Audit entries are hash-chained for tamper evidence.
        
        **RBAC:** `ib_admin` only
        **Rate limit:** 30 requests/minute per user
      operationId: getAuditLog
      security:
        - bearerAuth: []
      parameters:
        - name: action
          in: query
          schema:
            type: string
          description: Filter by action type (e.g., "file.downloaded", "access.granted")
        - name: actor_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by actor
        - name: target_id
          in: query
          schema:
            type: string
            format: uuid
          description: Filter by target entry/user/file
        - name: from
          in: query
          schema:
            type: string
            format: date-time
          description: Start of time range
        - name: to
          in: query
          schema:
            type: string
            format: date-time
          description: End of time range
        - name: limit
          in: query
          schema:
            type: integer
            default: 100
            maximum: 500
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Audit log entries
          content:
            application/json:
              schema:
                type: object
                properties:
                  entries:
                    type: array
                    items:
                      $ref: '#/components/schemas/AuditEntry'
                  total:
                    type: integer
                  limit:
                    type: integer
                  offset:
                    type: integer
                  chain_verified:
                    type: boolean
                    description: Whether hash chain integrity was verified
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'

  # ============================================================================
  # HEALTH ENDPOINT
  # ============================================================================
  
  /health:
    get:
      tags: [Health]
      summary: Service health check
      description: |
        Returns service health status. No authentication required.
        Suitable for load balancer and monitoring probes.
      operationId: healthCheck
      security: []
      responses:
        '200':
          description: Service healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [healthy, degraded]
                  version:
                    type: string
                    example: "1.0.0"
                  uptime:
                    type: integer
                    description: Uptime in seconds
                  checks:
                    type: object
                    properties:
                      database:
                        type: string
                        enum: [ok, error]
                      object_store:
                        type: string
                        enum: [ok, error]
                      redis:
                        type: string
                        enum: [ok, error]
        '503':
          description: Service unhealthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [unhealthy]
                  errors:
                    type: array
                    items:
                      type: string

# ==============================================================================
# COMPONENTS
# ==============================================================================

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: |
        JWT token from `/auth/login` or `/auth/refresh`.
        Token contains: sub (user_id), org, roles[], mfa, sid, fp, exp.

  parameters:
    ProjectId:
      name: id
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Project ID
      
    WorkstreamId:
      name: wsId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Workstream ID
      
    RequestId:
      name: reqId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Request ID
      
    AnswerId:
      name: ansId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Answer ID
      
    AnswerIdPath:
      name: answerId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Answer ID for matching
      
    ObjectId:
      name: objectId
      in: path
      required: true
      schema:
        type: string
        pattern: '^[a-f0-9]{16}$'
      description: Content-addressable object ID
      
    GrantId:
      name: grantId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Access grant ID
      
    InviteId:
      name: inviteId
      in: path
      required: true
      schema:
        type: string
        format: uuid
      description: Invite ID

  responses:
    Unauthorized:
      description: Authentication required or token invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Invalid or expired authentication token"
            code: "UNAUTHORIZED"
            details: {}
            
    Forbidden:
      description: Insufficient permissions for this operation
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "You do not have permission to perform this action"
            code: "FORBIDDEN"
            details:
              required_role: "ib_admin"
              
    NotFound:
      description: Resource not found
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Resource not found"
            code: "NOT_FOUND"
            details: {}
            
    Conflict:
      description: Concurrent modification conflict
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Resource was modified by another user"
            code: "CONFLICT"
            details:
              current_version: 5
              
    PreconditionFailed:
      description: ETag mismatch - resource changed since last fetch
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: "Resource has been modified. Fetch the latest version and retry."
            code: "PRECONDITION_FAILED"
            details:
              current_etag: '"abc123"'

  schemas:
    Error:
      type: object
      required: [error, code]
      properties:
        error:
          type: string
          description: Human-readable error message
        code:
          type: string
          description: Machine-readable error code
          enum:
            - BAD_REQUEST
            - UNAUTHORIZED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - PRECONDITION_FAILED
            - RATE_LIMIT_EXCEEDED
            - INTERNAL_ERROR
            - INVALID_INVITE
            - INVITE_EXPIRED
            - INVITE_ALREADY_USED
            - EMAIL_MISMATCH
            - MFA_REQUIRED
            - INVALID_TOTP
            - CONCURRENT_MODIFICATION
        details:
          type: object
          additionalProperties: true
          description: Additional error context

    AuthTokens:
      type: object
      properties:
        access_token:
          type: string
          description: JWT access token (1 hour lifetime)
        refresh_token:
          type: string
          description: Refresh token (7 day lifetime, rotates on use)
        token_type:
          type: string
          enum: [Bearer]
        expires_in:
          type: integer
          description: Access token lifetime in seconds
        mfa_required:
          type: boolean
          description: Whether MFA verification is pending
        user:
          $ref: '#/components/schemas/User'

    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
        organization_id:
          type: string
          format: uuid
        organization_name:
          type: string
        mfa_enabled:
          type: boolean
        created_at:
          type: string
          format: date-time

    Project:
      type: object
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
          example: "Project Falcon"
        description:
          type: string
        stage:
          type: string
          enum: [pre_dataroom, dataroom, closed]
        theme_id:
          type: string
          format: uuid
          nullable: true
        created_by:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        stats:
          type: object
          properties:
            workstreams:
              type: integer
            requests_total:
              type: integer
            requests_open:
              type: integer
            requests_completed:
              type: integer
            answers_published:
              type: integer
        my_role:
          type: string
          enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
          description: Current user's role in this project

    ProjectCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 2000
        theme_id:
          type: string
          format: uuid

    ProjectUpdate:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 2000
        stage:
          type: string
          enum: [pre_dataroom, dataroom, closed]
        theme_id:
          type: string
          format: uuid
          nullable: true

    Workstream:
      type: object
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        name:
          type: string
          example: "Finance"
        slug:
          type: string
          example: "finance"
        description:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        stats:
          type: object
          properties:
            requests_total:
              type: integer
            requests_open:
              type: integer
            answers_published:
              type: integer

    WorkstreamCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        slug:
          type: string
          pattern: '^[a-z0-9-]+$'
          maxLength: 50
          description: URL-safe identifier (auto-generated from name if not provided)
        description:
          type: string
          maxLength: 1000

    WorkstreamUpdate:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        description:
          type: string
          maxLength: 1000

    RequestList:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workstream_id:
          type: string
          format: uuid
        name:
          type: string
          example: "Initial Due Diligence"
        description:
          type: string
        created_by:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        request_count:
          type: integer

    RequestListCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
          maxLength: 1000

    Request:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workstream_id:
          type: string
          format: uuid
        list_id:
          type: string
          format: uuid
          nullable: true
        ref:
          type: string
          example: "FIN-042"
          description: Human-readable reference number
        title:
          type: string
          example: "Provide audited financials FY2024"
        body:
          type: string
        priority:
          type: string
          enum: [high, normal, low]
        due_date:
          type: string
          format: date
          nullable: true
        status:
          type: string
          enum: [open, assigned, answered, vetted, published, closed]
        stage:
          type: string
          enum: [pre_dataroom, dataroom, closed]
        assignee_id:
          type: string
          format: uuid
          nullable: true
          description: Current assignee (who has it RIGHT NOW)
        assignee:
          $ref: '#/components/schemas/User'
        return_to_id:
          type: string
          format: uuid
          nullable: true
          description: Who it returns to when completed
        origin_id:
          type: string
          format: uuid
          nullable: true
          description: Ultimate requester who started the chain
        created_by:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        routing_chain:
          type: array
          description: Full routing history (visible to IB only)
          items:
            type: object
            properties:
              actor_id:
                type: string
                format: uuid
              action:
                type: string
                enum: [created, forwarded, completed]
              timestamp:
                type: string
                format: date-time
              message:
                type: string
        linked_answers:
          type: array
          items:
            type: object
            properties:
              answer_id:
                type: string
                format: uuid
              confirmed:
                type: boolean
              linked_at:
                type: string
                format: date-time

    RequestCreate:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        body:
          type: string
          maxLength: 10000
        list_id:
          type: string
          format: uuid
          description: Optional request list to add to
        priority:
          type: string
          enum: [high, normal, low]
          default: normal
        due_date:
          type: string
          format: date
        assigned_to:
          type: array
          items:
            type: string
            format: uuid
          description: Initial assignees

    RequestUpdate:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        body:
          type: string
          maxLength: 10000
        priority:
          type: string
          enum: [high, normal, low]
        due_date:
          type: string
          format: date
          nullable: true
        assigned_to:
          type: array
          items:
            type: string
            format: uuid
        status:
          type: string
          enum: [open, assigned, answered, vetted, published, closed]

    Answer:
      type: object
      properties:
        id:
          type: string
          format: uuid
        workstream_id:
          type: string
          format: uuid
        title:
          type: string
        body:
          type: string
        status:
          type: string
          enum: [draft, submitted, approved, rejected, published]
        rejection_reason:
          type: string
          nullable: true
        files:
          type: array
          items:
            type: object
            properties:
              object_id:
                type: string
                pattern: '^[a-f0-9]{16}$'
              filename:
                type: string
              size:
                type: integer
              mime_type:
                type: string
        linked_requests:
          type: array
          items:
            type: object
            properties:
              request_id:
                type: string
                format: uuid
              ref:
                type: string
              title:
                type: string
              confirmed:
                type: boolean
        broadcast_to:
          type: string
          enum: [linked_requesters, all_workstream, all_dataroom]
          nullable: true
        created_by:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        submitted_at:
          type: string
          format: date-time
          nullable: true
        approved_at:
          type: string
          format: date-time
          nullable: true
        published_at:
          type: string
          format: date-time
          nullable: true

    AnswerCreate:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        body:
          type: string
          maxLength: 50000
        file_ids:
          type: array
          items:
            type: string
            pattern: '^[a-f0-9]{16}$'
          description: Object IDs of uploaded files
        request_ids:
          type: array
          items:
            type: string
            format: uuid
          description: Requests this answer is linked to

    AnswerUpdate:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 500
        body:
          type: string
          maxLength: 50000
        file_ids:
          type: array
          items:
            type: string
            pattern: '^[a-f0-9]{16}$'
        request_ids:
          type: array
          items:
            type: string
            format: uuid

    AnswerMatch:
      type: object
      properties:
        answer_id:
          type: string
          format: uuid
        answer_title:
          type: string
        answer_status:
          type: string
          enum: [published]
        similarity_score:
          type: number
          format: float
          minimum: 0
          maximum: 1
          description: Cosine similarity score (AI-computed)
        matched_text:
          type: string
          description: Snippet of answer that matched the request
        linked_requests:
          type: integer
          description: Number of other requests already linked to this answer

    Task:
      type: object
      description: A request in the user's personal inbox
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        project_name:
          type: string
        workstream_id:
          type: string
          format: uuid
        workstream_name:
          type: string
        ref:
          type: string
        title:
          type: string
        priority:
          type: string
          enum: [high, normal, low]
        due_date:
          type: string
          format: date
          nullable: true
        status:
          type: string
          enum: [open, assigned, answered, vetted, published, closed]
        is_overdue:
          type: boolean
        return_to:
          $ref: '#/components/schemas/User'
        forwarded_by:
          $ref: '#/components/schemas/User'
        assigned_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    AccessGrant:
      type: object
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        workstream_id:
          type: string
          format: uuid
          nullable: true
          description: If null, access applies to all workstreams
        user_id:
          type: string
          format: uuid
        user:
          $ref: '#/components/schemas/User'
        role:
          type: string
          enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
        ops:
          type: string
          description: "Operation flags: r=read, w=write, d=delete, m=manage"
          example: "rw"
        granted_by:
          type: string
          format: uuid
        granted_at:
          type: string
          format: date-time

    AccessGrantCreate:
      type: object
      required: [user_id, role]
      properties:
        user_id:
          type: string
          format: uuid
        role:
          type: string
          enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
        workstream_id:
          type: string
          format: uuid
          nullable: true
          description: Scope to specific workstream (null = all workstreams)
        ops:
          type: string
          default: "rw"
          description: "Operation flags: r=read, w=write, d=delete, m=manage"

    Invite:
      type: object
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        workstream_id:
          type: string
          format: uuid
          nullable: true
        email:
          type: string
          format: email
        role:
          type: string
          enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
        status:
          type: string
          enum: [pending, accepted, expired, revoked]
        invited_by:
          type: string
          format: uuid
        invited_by_name:
          type: string
        expires_at:
          type: string
          format: date-time
        accepted_at:
          type: string
          format: date-time
          nullable: true
        created_at:
          type: string
          format: date-time

    InviteCreate:
      type: object
      required: [email, role]
      properties:
        email:
          type: string
          format: email
        role:
          type: string
          enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer]
        workstream_id:
          type: string
          format: uuid
          nullable: true
          description: Scope to specific workstream
        message:
          type: string
          maxLength: 1000
          description: Personal message to include in invite email

    AuditEntry:
      type: object
      properties:
        id:
          type: string
          format: uuid
        project_id:
          type: string
          format: uuid
        actor_id:
          type: string
          format: uuid
        actor_email:
          type: string
          format: email
        action:
          type: string
          description: |
            Event type. Examples:
            - auth.login, auth.logout, auth.mfa_enabled
            - access.granted, access.revoked
            - entry.created, entry.updated, entry.published
            - file.uploaded, file.downloaded, file.deleted
            - session.created, session.revoked
        target_type:
          type: string
          enum: [entry, user, access, session, file]
          nullable: true
        target_id:
          type: string
          format: uuid
          nullable: true
        details:
          type: object
          additionalProperties: true
          description: Action-specific details
        ip_address:
          type: string
        user_agent:
          type: string
        timestamp:
          type: string
          format: date-time
        hash:
          type: string
          description: SHA-256 hash for tamper detection (chain integrity)
