openapi: 3.1.0
info:
  title: DecideNext API
  version: "1.0.0"
  description: >
    Token-authenticated HTTP API for managing a single DecideNext prioritization
    table — its initiatives, voters, results, comments, sharing, and backups —
    from a script or AI agent, without the dashboard UI.


    Authenticate every request with an HTTP bearer token (`dnx_live_…`). The
    token identifies the table, so no manager key or table ID appears in the URL.
    Each token is limited to 10 requests per minute and expires after 90 days;
    generate a new one from the manager dashboard's API Access tab. Deleting the
    table and managing tokens stay dashboard-only and are not exposed here.
servers:
  - url: https://decidenext.app/api/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Table
  - name: Criteria
  - name: Initiatives
  - name: Comments
  - name: Voters
  - name: Results & progress
  - name: Sharing & backup
  - name: Retro Board
  - name: Retro Participants
  - name: Retro Cards
  - name: Retro Results & Export
paths:
  /table:
    get:
      tags: [Table]
      summary: Get table details
      operationId: getTable
      responses:
        "200":
          description: The table the token belongs to.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TableResponse"
              example:
                id: 2c9d1e7a-1f3b-4a2c-9f1e-8b7a6c5d4e3f
                name: Q4 Roadmap
                description: Candidate initiatives for next quarter
                methodology: ICE
                created_at: "2026-01-15T09:30:00Z"
                updated_at: "2026-01-20T14:05:00Z"
                is_active: true
                has_voter_password: false
                public_share_enabled: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    put:
      tags: [Table]
      summary: Update table name, description, active state, or budget
      operationId: updateTable
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTableRequest"
            example:
              name: Q4 Roadmap (revised)
              description: Updated candidate list
      responses:
        "200":
          description: The updated table.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TableResponse"
              example:
                id: 2c9d1e7a-1f3b-4a2c-9f1e-8b7a6c5d4e3f
                name: Q4 Roadmap (revised)
                description: Updated candidate list
                methodology: ICE
                created_at: "2026-01-15T09:30:00Z"
                updated_at: "2026-01-21T10:00:00Z"
                is_active: true
                has_voter_password: false
                public_share_enabled: false
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /criteria:
    get:
      tags: [Criteria]
      summary: List Weighted Decision Matrix criteria
      description: >
        Only valid for tables using the WEIGHTED methodology. Other methodologies
        return 400 `invalid_methodology`.
      operationId: getCriteria
      responses:
        "200":
          description: The weighted criteria for the table.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WeightedCriterionResponse"
              example:
                - id: a1b2c3d4-0000-4000-8000-000000000001
                  name: Impact
                  weight: 40
                  display_order: 1
                  min_value: 1
                  max_value: 5
                  description: Expected business impact
                - id: a1b2c3d4-0000-4000-8000-000000000002
                  name: Cost
                  weight: 60
                  display_order: 2
                  min_value: 1
                  max_value: 5
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /initiatives:
    get:
      tags: [Initiatives]
      summary: List initiatives
      operationId: listInitiatives
      responses:
        "200":
          description: All initiatives in the table, with comment counts.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InitiativeResponse"
              example:
                - id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                  title: Launch onboarding revamp
                  description: Q4 priority
                  tags: [growth, onboarding]
                  display_order: 1
                  status: active
                  created_at: "2026-01-16T08:00:00Z"
                  updated_at: "2026-01-16T08:00:00Z"
                  comment_count: 2
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Initiatives]
      summary: Create an initiative
      operationId: createInitiative
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateInitiativeRequest"
            example:
              title: Launch onboarding revamp
              description: Q4 priority
              tags: [growth, onboarding]
      responses:
        "201":
          description: The created initiative.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InitiativeResponse"
              example:
                id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                title: Launch onboarding revamp
                description: Q4 priority
                tags: [growth, onboarding]
                display_order: 1
                status: active
                created_at: "2026-01-16T08:00:00Z"
                updated_at: "2026-01-16T08:00:00Z"
                comment_count: 0
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /initiatives/reorder:
    put:
      tags: [Initiatives]
      summary: Reorder initiatives
      description: >
        Sets the display order of initiatives to match the order of the supplied
        IDs. Every ID must belong to the authenticated table.
      operationId: reorderInitiatives
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ReorderInitiativesRequest"
            example:
              initiative_ids:
                - 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                - 9a8b7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d
      responses:
        "200":
          description: Reorder acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Initiatives reordered successfully
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /initiatives/bulk:
    post:
      tags: [Initiatives]
      summary: Bulk import initiatives
      description: >
        Imports up to 50 initiatives at once. Titles matching existing
        initiatives (case-insensitive) are skipped as duplicates.
      operationId: bulkImportInitiatives
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/BulkImportInitiativesRequest"
            example:
              initiatives:
                - title: Improve search relevance
                  description: Tune ranking
                  tags: [search]
                - title: Dark mode
      responses:
        "201":
          description: Import summary.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BulkImportInitiativesResponse"
              example:
                imported: 1
                skipped: 1
                skipped_titles: [Dark mode]
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /initiatives/{id}:
    parameters:
      - $ref: "#/components/parameters/InitiativeId"
    get:
      tags: [Initiatives]
      summary: Get a single initiative
      operationId: getInitiative
      responses:
        "200":
          description: The requested initiative.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InitiativeResponse"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    put:
      tags: [Initiatives]
      summary: Update an initiative
      operationId: updateInitiative
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateInitiativeRequest"
            example:
              title: Launch onboarding revamp v2
              status: active
      responses:
        "200":
          description: The updated initiative.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InitiativeResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    delete:
      tags: [Initiatives]
      summary: Delete an initiative
      operationId: deleteInitiative
      responses:
        "200":
          description: Deletion acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Initiative deleted successfully
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /initiatives/{id}/comments:
    parameters:
      - $ref: "#/components/parameters/InitiativeId"
    get:
      tags: [Comments]
      summary: List comments on an initiative
      operationId: listComments
      responses:
        "200":
          description: The initiative's comments, chronological.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommentsListResponse"
              example:
                comments:
                  - id: c0ffee00-0000-4000-8000-000000000001
                    initiative_id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                    author_name: Manager
                    is_manager: true
                    content: Let's prioritize this.
                    created_at: "2026-01-18T12:00:00Z"
                total_count: 1
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Comments]
      summary: Add a comment to an initiative
      description: >
        Posts a comment as the manager (author shown as "Manager"). Content is
        1–180 characters. Per-initiative comment limits apply.
      operationId: createComment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateCommentRequest"
            example:
              content: Let's prioritize this.
      responses:
        "201":
          description: The created comment.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CommentResponse"
              example:
                id: c0ffee00-0000-4000-8000-000000000001
                initiative_id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                author_name: Manager
                is_manager: true
                content: Let's prioritize this.
                created_at: "2026-01-18T12:00:00Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /users:
    get:
      tags: [Voters]
      summary: List voters
      operationId: listUsers
      responses:
        "200":
          description: Voters on the table, with participation status.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/UserResponse"
              example:
                - id: 11111111-2222-4333-8444-555555555555
                  name: Alex
                  email: alex@example.com
                  access_token: aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                  voter_url: https://decidenext.app/vote/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                  has_voted: true
                  last_voted_at: "2026-01-19T16:20:00Z"
                  created_at: "2026-01-16T09:00:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Voters]
      summary: Add a voter
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUserRequest"
            example:
              name: Alex
              email: alex@example.com
      responses:
        "201":
          description: The created voter, including the unique voting URL.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserResponse"
              example:
                id: 11111111-2222-4333-8444-555555555555
                name: Alex
                email: alex@example.com
                access_token: aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                voter_url: https://decidenext.app/vote/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                has_voted: false
                created_at: "2026-01-16T09:00:00Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /users/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Voter (user) UUID.
        schema:
          type: string
          format: uuid
    delete:
      tags: [Voters]
      summary: Remove a voter
      operationId: deleteUser
      responses:
        "200":
          description: Deletion acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Voter deleted successfully
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /voter-password:
    put:
      tags: [Voters]
      summary: Set or update the voter password
      description: >
        Sets a single table-wide voter password (minimum 6 characters) shared by
        all voters.
      operationId: setVoterPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetVoterPasswordRequest"
            example:
              password: team-secret-2026
      responses:
        "200":
          description: Password set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Voter password set successfully
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /results:
    get:
      tags: [Results & progress]
      summary: Get calculated results
      description: >
        Returns ranked results with aggregated scores and per-voter breakdowns.
        Which optional fields are populated depends on the table's methodology.
      operationId: getResults
      responses:
        "200":
          description: Calculated, ranked results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ResultsResponse"
              example:
                table_name: Q4 Roadmap
                methodology: ICE
                total_voters: 3
                voted_count: 2
                initiatives:
                  - id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                    title: Launch onboarding revamp
                    tags: [growth]
                    rank: 1
                    calculated_score: 72.5
                    vote_count: 2
                    average_scores:
                      ice_impact: 8
                      ice_confidence: 90
                      ice_ease: 7
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /voter-rankings:
    get:
      tags: [Results & progress]
      summary: Get per-voter rankings
      operationId: getVoterRankings
      responses:
        "200":
          description: Each voter's ranking of each initiative, with disagreement flags.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/VoterRankingsResponse"
              example:
                methodology: ICE
                voters:
                  - id: 11111111-2222-4333-8444-555555555555
                    name: Alex
                initiatives:
                  - id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                    title: Launch onboarding revamp
                    voter_rankings:
                      "11111111-2222-4333-8444-555555555555":
                        rank: 1
                        score: 72.5
                        voted: true
                    rank_std_dev: 0
                    has_disagreement: false
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /progress:
    get:
      tags: [Results & progress]
      summary: Get voting progress
      operationId: getProgress
      responses:
        "200":
          description: How many voters have voted so far.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProgressResponse"
              example:
                total_voters: 3
                voted_count: 2
                not_voted_count: 1
                percentage: 66.67
                voters:
                  - id: 11111111-2222-4333-8444-555555555555
                    name: Alex
                    has_voted: true
                    last_voted_at: "2026-01-19T16:20:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /public-share:
    put:
      tags: [Sharing & backup]
      summary: Enable or disable the public results URL
      operationId: togglePublicShare
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TogglePublicShareRequest"
            example:
              enabled: true
      responses:
        "200":
          description: The updated table, including the public share key when enabled.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TableResponse"
              example:
                id: 2c9d1e7a-1f3b-4a2c-9f1e-8b7a6c5d4e3f
                name: Q4 Roadmap
                methodology: ICE
                created_at: "2026-01-15T09:30:00Z"
                updated_at: "2026-01-21T10:00:00Z"
                is_active: true
                has_voter_password: false
                public_share_enabled: true
                public_share_key: dddddddd-eeee-4fff-8aaa-bbbbbbbbbbbb
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /backup:
    get:
      tags: [Sharing & backup]
      summary: Export a full table backup
      description: >
        Returns the entire table — settings, initiatives, voters, scores,
        comments, and (where present) paired-comparison votes and weighted
        criteria/scores — as a single JSON document.
      operationId: exportBackup
      responses:
        "200":
          description: The full table export.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BackupData"
              example:
                version: "1.0"
                exported_at: "2026-01-21T10:30:00Z"
                table:
                  name: Q4 Roadmap
                  methodology: ICE
                  is_active: true
                initiatives:
                  - original_id: 7f1e2d3c-4b5a-6978-8c7d-6e5f4a3b2c1d
                    title: Launch onboarding revamp
                    tags: [growth]
                    display_order: 1
                    status: active
                users:
                  - original_id: 11111111-2222-4333-8444-555555555555
                    name: Alex
                scores: []
                comments: []
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/board:
    get:
      tags: [Retro Board]
      summary: Get retro board details
      security:
        - retroBearerAuth: []
      operationId: getRetroBoard
      responses:
        "200":
          description: The retro board the token belongs to.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetroBoardResponse"
              example:
                id: b1a2c3d4-0000-4000-8000-000000000001
                name: Sprint 24 Retro
                description: What went well, what didn't
                board_type: STARFISH
                votes_per_user: 5
                is_active: true
                has_voter_password: true
                created_at: "2026-02-10T09:00:00Z"
                updated_at: "2026-02-10T09:00:00Z"
                columns:
                  - id: c1a2c3d4-0000-4000-8000-000000000001
                    name: Keep doing
                    display_order: 1
                    color: "#22C55E"
                  - id: c1a2c3d4-0000-4000-8000-000000000002
                    name: Stop doing
                    display_order: 2
                    color: "#EF4444"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    put:
      tags: [Retro Board]
      summary: Update board settings
      description: All fields optional; only supplied fields are updated.
      security:
        - retroBearerAuth: []
      operationId: updateRetroBoard
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateRetroBoardRequest"
            example:
              name: Sprint 24 Retro (closed)
              is_active: false
      responses:
        "200":
          description: The updated board.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetroBoardResponse"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/voter-password:
    put:
      tags: [Retro Board]
      summary: Set the participant password
      description: >
        Sets a single board-wide participant password (minimum 6 characters)
        shared by everyone voting on the board.
      security:
        - retroBearerAuth: []
      operationId: setRetroVoterPassword
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SetRetroVoterPasswordRequest"
            example:
              password: team-secret-2026
      responses:
        "200":
          description: Password set.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Voter password set successfully
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/users:
    get:
      tags: [Retro Participants]
      summary: List participants
      security:
        - retroBearerAuth: []
      operationId: listRetroUsers
      responses:
        "200":
          description: Participants on the board, with votes used.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RetroUserResponse"
              example:
                - id: 11111111-2222-4333-8444-555555555555
                  name: Alex
                  access_token: aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                  voter_url: https://decidenext.app/login/retro-voter/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                  votes_used: 3
                  created_at: "2026-02-10T09:05:00Z"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
    post:
      tags: [Retro Participants]
      summary: Add a participant
      description: >
        name is the only field (retro participants have no email). The
        response includes the participant's unique voting URL.
      security:
        - retroBearerAuth: []
      operationId: createRetroUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRetroUserRequest"
            example:
              name: Alex
      responses:
        "201":
          description: The created participant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetroUserResponse"
              example:
                id: 11111111-2222-4333-8444-555555555555
                name: Alex
                access_token: aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                voter_url: https://decidenext.app/login/retro-voter/aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee
                votes_used: 0
                created_at: "2026-02-10T09:05:00Z"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/users/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: Participant (user) UUID.
        schema:
          type: string
          format: uuid
    delete:
      tags: [Retro Participants]
      summary: Remove a participant
      security:
        - retroBearerAuth: []
      operationId: deleteRetroUser
      responses:
        "200":
          description: Deletion acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: User deleted successfully
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/cards:
    get:
      tags: [Retro Cards]
      summary: List cards
      description: >
        All active (non-merged) cards on the board with their vote counts.
        has_voted is always false in this token/manager context — it is only
        meaningful for an individual voter session.
      security:
        - retroBearerAuth: []
      operationId: listRetroCards
      responses:
        "200":
          description: The board's cards.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/RetroCardResponse"
              example:
                - id: d1a2c3d4-0000-4000-8000-000000000001
                  column_id: c1a2c3d4-0000-4000-8000-000000000001
                  content: We shipped the migration without a rollback plan
                  vote_count: 4
                  has_voted: false
                  created_at: "2026-02-10T09:10:00Z"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/cards/{id}/merge:
    parameters:
      - name: id
        in: path
        required: true
        description: >
          UUID of the card to merge. This card is merged into the card given
          by target_card_id in the request body.
        schema:
          type: string
          format: uuid
    post:
      tags: [Retro Cards]
      summary: Merge one card into another
      description: Both cards must belong to this board and neither may already be merged.
      security:
        - retroBearerAuth: []
      operationId: mergeRetroCards
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MergeRetroCardRequest"
            example:
              target_card_id: d1a2c3d4-0000-4000-8000-000000000002
      responses:
        "200":
          description: Merge acknowledged.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SuccessResponse"
              example:
                message: Cards merged successfully
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/results:
    get:
      tags: [Retro Results & Export]
      summary: Get grouped, vote-ranked results
      description: >
        Every column with its cards sorted by vote_count descending, plus
        per-participant vote usage.
      security:
        - retroBearerAuth: []
      operationId: getRetroResults
      responses:
        "200":
          description: The board's results.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RetroResultsResponse"
              example:
                board_name: Sprint 24 Retro
                board_type: STARFISH
                total_voters: 4
                total_cards: 9
                total_votes: 23
                columns:
                  - id: c1a2c3d4-0000-4000-8000-000000000001
                    name: Keep doing
                    color: "#22C55E"
                    cards:
                      - id: d1a2c3d4-0000-4000-8000-000000000001
                        column_id: c1a2c3d4-0000-4000-8000-000000000001
                        content: We shipped the migration without a rollback plan
                        vote_count: 4
                        has_voted: false
                        created_at: "2026-02-10T09:10:00Z"
                participation:
                  - user_id: 11111111-2222-4333-8444-555555555555
                    user_name: Alex
                    votes_used: 3
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/export:
    post:
      tags: [Retro Results & Export]
      summary: Export cards to a new prioritization table
      description: >
        Creates a brand-new prioritization table (secured by manager_password)
        with one initiative per card. edits optionally rewrites a card's title
        before import; include_users also copies the board's participants in
        as voters on the new table.
      security:
        - retroBearerAuth: []
      operationId: exportRetroCards
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExportRetroCardsRequest"
            example:
              card_ids:
                - d1a2c3d4-0000-4000-8000-000000000001
                - d1a2c3d4-0000-4000-8000-000000000002
              methodology: ICE
              table_name: Sprint 24 Action Items
              manager_password: new-table-secret
              include_users: true
      responses:
        "201":
          description: The newly created prioritization table.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreateTableResponse"
              example:
                table_id: e1a2c3d4-0000-4000-8000-000000000001
                manager_key: f1a2c3d4-0000-4000-8000-000000000001
                manager_url: https://decidenext.app/manager/f1a2c3d4-0000-4000-8000-000000000001
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/RetroUnauthorized"
        "429":
          $ref: "#/components/responses/RateLimited"
  /retro/export-to-existing:
    post:
      tags: [Retro Results & Export]
      summary: Export cards into an existing prioritization table
      description: >
        Adds one initiative per card to an existing prioritization table,
        authenticated with that table's own manager_key/password (not the
        retro token's board).
      security:
        - retroBearerAuth: []
      operationId: exportRetroCardsToExisting
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ExportRetroCardsToExistingRequest"
            example:
              card_ids:
                - d1a2c3d4-0000-4000-8000-000000000001
              target_manager_key: f1a2c3d4-0000-4000-8000-000000000001
              target_password: existing-table-secret
      responses:
        "201":
          description: The target table's manager key, confirming the import.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ExportToExistingResponse"
              example:
                manager_key: f1a2c3d4-0000-4000-8000-000000000001
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          description: >
            Missing/invalid retro token (`unauthorized`/`token_expired`), or
            the target table's manager_key/target_password was rejected
            (`unauthorized`).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ErrorResponse"
              example:
                error: unauthorized
                message: Invalid target table credentials
        "429":
          $ref: "#/components/responses/RateLimited"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: >
        API token issued from the manager dashboard's API Access tab. Send it as
        `Authorization: Bearer dnx_live_…`. One active token per table; expires
        after 90 days.
    retroBearerAuth:
      type: http
      scheme: bearer
      description: >
        Separate API token for the retrospective API, issued from the retro
        board's dashboard API Access tab. Send it as `Authorization: Bearer
        dnx_retro_…`. One active token per board; expires after 90 days. Only
        valid against `/retro/*` paths — it is rejected on the rest of
        `/api/v1`, and a `dnx_live_…` token is rejected here.
  parameters:
    InitiativeId:
      name: id
      in: path
      required: true
      description: Initiative UUID.
      schema:
        type: string
        format: uuid
  responses:
    Unauthorized:
      description: >
        Missing/invalid token (`unauthorized`) or an expired token
        (`token_expired`).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unauthorized:
              value:
                error: unauthorized
                message: "Authorization: Bearer <token> required"
            token_expired:
              value:
                error: token_expired
                message: API token expired. Generate a new one.
    NotFound:
      description: The referenced resource does not exist in this table.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: not_found
            message: Initiative not found
    ValidationError:
      description: The request body failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: validation_error
            message: "Key: 'CreateInitiativeRequest.Title' Error:Field validation for 'Title' failed on the 'required' tag"
    RateLimited:
      description: >
        Rate limit of 10 requests/minute exceeded. A `Retry-After` header gives
        the number of seconds to wait.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
            example: 60
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: rate_limited
            message: Rate limit exceeded. Try again shortly.
    RetroUnauthorized:
      description: >
        Missing/invalid retro token (`unauthorized`) or an expired retro token
        (`token_expired`). A `dnx_live_…` prioritization token on this path
        also fails with `unauthorized`.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          examples:
            unauthorized:
              value:
                error: unauthorized
                message: "Authorization: Bearer <token> required"
            invalid_token:
              value:
                error: unauthorized
                message: Invalid API token
            token_expired:
              value:
                error: token_expired
                message: API token expired. Generate a new one.
    Forbidden:
      description: The referenced resource belongs to a different board.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: forbidden
            message: User does not belong to this board
    BadRequest:
      description: The request was malformed (e.g. an invalid UUID path segment).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/ErrorResponse"
          example:
            error: bad_request
            message: Invalid user ID
  schemas:
    ErrorResponse:
      type: object
      required: [error]
      properties:
        error:
          type: string
          description: Machine-readable error code.
        message:
          type: string
          description: Human-readable detail.
    SuccessResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
        data:
          description: Optional payload; usually omitted.
    TableResponse:
      type: object
      required:
        - id
        - name
        - methodology
        - created_at
        - updated_at
        - is_active
        - has_voter_password
        - public_share_enabled
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        methodology:
          type: string
          enum: [ICE, RICE, MOSCOW, BUYAFEATURE, WSJF, PAIREDCOMPARISON, WEIGHTED]
        buyafeature_budget_per_voter:
          type: number
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        is_active:
          type: boolean
        has_voter_password:
          type: boolean
        public_share_enabled:
          type: boolean
        public_share_key:
          type: string
    UpdateTableRequest:
      type: object
      description: All fields optional; only supplied fields are updated.
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 255
        description:
          type: string
        is_active:
          type: boolean
        buyafeature_budget_per_voter:
          type: number
          exclusiveMinimum: 0
    TogglePublicShareRequest:
      type: object
      required: [enabled]
      properties:
        enabled:
          type: boolean
          description: Whether the public results URL is enabled.
    WeightedCriterionResponse:
      type: object
      required: [id, name, weight, display_order, min_value, max_value]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        weight:
          type: number
          description: Percentage weight; all criteria sum to 100.
        display_order:
          type: integer
        min_value:
          type: integer
        max_value:
          type: integer
        description:
          type: string
    InitiativeResponse:
      type: object
      required:
        - id
        - title
        - tags
        - display_order
        - status
        - created_at
        - updated_at
        - comment_count
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
        tags:
          type: [array, "null"]
          items:
            type: string
        buyafeature_cost:
          type: number
        display_order:
          type: integer
        status:
          type: string
          enum: [active, archived]
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        comment_count:
          type: integer
    CreateInitiativeRequest:
      type: object
      required: [title]
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        tags:
          type: array
          items:
            type: string
        buyafeature_cost:
          type: number
          exclusiveMinimum: 0
          description: Required for Buy a Feature tables.
    UpdateInitiativeRequest:
      type: object
      description: All fields optional; only supplied fields are updated.
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 255
        description:
          type: string
        tags:
          type: array
          items:
            type: string
        buyafeature_cost:
          type: number
          exclusiveMinimum: 0
        display_order:
          type: integer
        status:
          type: string
          enum: [active, archived]
    ReorderInitiativesRequest:
      type: object
      required: [initiative_ids]
      properties:
        initiative_ids:
          type: array
          minItems: 1
          items:
            type: string
            format: uuid
    BulkImportInitiativesRequest:
      type: object
      required: [initiatives]
      properties:
        initiatives:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: "#/components/schemas/CreateInitiativeRequest"
    BulkImportInitiativesResponse:
      type: object
      required: [imported, skipped]
      properties:
        imported:
          type: integer
        skipped:
          type: integer
        skipped_titles:
          type: array
          items:
            type: string
    UserResponse:
      type: object
      required:
        - id
        - name
        - access_token
        - voter_url
        - has_voted
        - created_at
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        email:
          type: string
        access_token:
          type: string
          format: uuid
        voter_url:
          type: string
        has_voted:
          type: boolean
        last_voted_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time
    CreateUserRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
        email:
          type: string
          format: email
    SetVoterPasswordRequest:
      type: object
      required: [password]
      properties:
        password:
          type: string
          minLength: 6
    CommentResponse:
      type: object
      required: [id, initiative_id, author_name, is_manager, content, created_at]
      properties:
        id:
          type: string
          format: uuid
        initiative_id:
          type: string
          format: uuid
        author_name:
          type: string
          description: Voter name, or "Manager".
        is_manager:
          type: boolean
        content:
          type: string
        created_at:
          type: string
          format: date-time
    CommentsListResponse:
      type: object
      required: [comments, total_count]
      properties:
        comments:
          type: array
          items:
            $ref: "#/components/schemas/CommentResponse"
        total_count:
          type: integer
    CreateCommentRequest:
      type: object
      required: [content]
      properties:
        content:
          type: string
          minLength: 1
          maxLength: 180
    ProgressResponse:
      type: object
      required: [total_voters, voted_count, not_voted_count, percentage, voters]
      properties:
        total_voters:
          type: integer
        voted_count:
          type: integer
        not_voted_count:
          type: integer
        percentage:
          type: number
        voters:
          type: array
          items:
            $ref: "#/components/schemas/VoterStatus"
    VoterStatus:
      type: object
      required: [id, name, has_voted]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        has_voted:
          type: boolean
        last_voted_at:
          type: string
          format: date-time
    ResultsResponse:
      type: object
      required: [table_name, methodology, total_voters, voted_count, initiatives]
      properties:
        table_name:
          type: string
        methodology:
          type: string
        total_voters:
          type: integer
        voted_count:
          type: integer
        initiatives:
          type: array
          items:
            $ref: "#/components/schemas/InitiativeResult"
    InitiativeResult:
      type: object
      required: [id, title, tags, rank, calculated_score, vote_count]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        description:
          type: string
        tags:
          type: [array, "null"]
          items:
            type: string
        rank:
          type: integer
        calculated_score:
          type: number
        vote_count:
          type: integer
        average_scores:
          $ref: "#/components/schemas/AverageScores"
        moscow_distribution:
          $ref: "#/components/schemas/MoscowDistribution"
        final_category:
          type: string
          description: "For MoSCoW: the category with the most votes."
        buyafeature_cost:
          type: number
        buyafeature_total_investment:
          type: number
        buyafeature_buyer_count:
          type: integer
        wsjf_score:
          type: number
        wsjf_cost_of_delay:
          type: number
        wsjf_avg_job_size:
          type: number
        wsjf_averages:
          $ref: "#/components/schemas/WSJFAverageScores"
        paired_comparison_win_count:
          type: integer
        paired_comparison_win_rate:
          type: number
        weighted_score:
          type: number
        weighted_criterion_scores:
          type: array
          items:
            $ref: "#/components/schemas/WeightedCriterionScore"
        consensus_level:
          type: string
          description: '"high", "medium", or "low".'
        alignment_score:
          type: number
        score_std_dev:
          type: number
        score_min:
          type: number
        score_max:
          type: number
        individual_votes:
          type: array
          items:
            $ref: "#/components/schemas/IndividualVote"
    AverageScores:
      type: object
      description: Average component scores and standard deviations (ICE/RICE).
      properties:
        ice_impact:
          type: number
        ice_confidence:
          type: number
        ice_ease:
          type: number
        ice_impact_std_dev:
          type: number
        ice_confidence_std_dev:
          type: number
        ice_ease_std_dev:
          type: number
        rice_reach:
          type: number
        rice_impact:
          type: number
        rice_confidence:
          type: number
        rice_effort:
          type: number
        rice_reach_std_dev:
          type: number
        rice_impact_std_dev:
          type: number
        rice_confidence_std_dev:
          type: number
        rice_effort_std_dev:
          type: number
    MoscowDistribution:
      type: object
      required: [must, should, could, wont, is_polarized]
      properties:
        must:
          type: integer
        should:
          type: integer
        could:
          type: integer
        wont:
          type: integer
        is_polarized:
          type: boolean
          description: True if both Must and Won't have votes.
    WSJFAverageScores:
      type: object
      properties:
        avg_business_value:
          type: number
        avg_time_criticality:
          type: number
        avg_risk_reduction:
          type: number
        avg_job_size:
          type: number
        business_value_std_dev:
          type: number
        time_criticality_std_dev:
          type: number
        risk_reduction_std_dev:
          type: number
        job_size_std_dev:
          type: number
    WeightedCriterionScore:
      type: object
      required: [criterion_id, criterion_name, weight, average_score, weighted_value]
      properties:
        criterion_id:
          type: string
          format: uuid
        criterion_name:
          type: string
        weight:
          type: number
        average_score:
          type: number
        weighted_value:
          type: number
          description: average_score * weight / 100
    IndividualVote:
      type: object
      required: [voter_id, voter_name, calculated_score]
      properties:
        voter_id:
          type: string
          format: uuid
        voter_name:
          type: string
        calculated_score:
          type: number
        ice_impact:
          type: integer
        ice_confidence:
          type: integer
        ice_ease:
          type: integer
        rice_reach:
          type: integer
        rice_impact:
          type: integer
        rice_confidence:
          type: integer
        rice_effort:
          type: integer
        moscow_category:
          type: string
        buyafeature_purchased:
          type: boolean
        buyafeature_amount_invested:
          type: number
        wsjf_business_value:
          type: integer
        wsjf_time_criticality:
          type: integer
        wsjf_risk_reduction:
          type: integer
        wsjf_job_size:
          type: integer
        paired_comparison_rank:
          type: integer
        weighted_criterion_scores:
          type: object
          additionalProperties:
            type: integer
          description: criterion_id -> score value
    VoterRankingsResponse:
      type: object
      required: [voters, initiatives, methodology]
      properties:
        voters:
          type: array
          items:
            $ref: "#/components/schemas/VoterInfo"
        initiatives:
          type: array
          items:
            $ref: "#/components/schemas/InitiativeRankings"
        methodology:
          type: string
    VoterInfo:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
    InitiativeRankings:
      type: object
      required: [id, title, voter_rankings, rank_std_dev, has_disagreement]
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        voter_rankings:
          type: object
          additionalProperties:
            $ref: "#/components/schemas/VoterRank"
          description: Keyed by voter_id.
        rank_std_dev:
          type: number
        has_disagreement:
          type: boolean
    VoterRank:
      type: object
      required: [voted]
      properties:
        rank:
          type: integer
        score:
          type: number
        voted:
          type: boolean
        buyafeature_purchased:
          type: boolean
    BackupData:
      type: object
      required: [version, exported_at, table, initiatives, users, scores, comments]
      properties:
        version:
          type: string
        exported_at:
          type: string
          format: date-time
        table:
          $ref: "#/components/schemas/BackupTable"
        initiatives:
          type: array
          items:
            $ref: "#/components/schemas/BackupInitiative"
        users:
          type: array
          items:
            $ref: "#/components/schemas/BackupUser"
        scores:
          type: array
          items:
            $ref: "#/components/schemas/BackupScore"
        comments:
          type: array
          items:
            $ref: "#/components/schemas/BackupComment"
        paired_comparison_votes:
          type: array
          items:
            $ref: "#/components/schemas/BackupPairedVote"
        weighted_criteria:
          type: array
          items:
            $ref: "#/components/schemas/BackupWeightedCriterion"
        weighted_scores:
          type: array
          items:
            $ref: "#/components/schemas/BackupWeightedScore"
    BackupTable:
      type: object
      required: [name, methodology, is_active]
      properties:
        name:
          type: string
        description:
          type: string
        methodology:
          type: string
        buyafeature_budget_per_voter:
          type: number
        is_active:
          type: boolean
    BackupInitiative:
      type: object
      required: [original_id, title, tags, display_order, status]
      properties:
        original_id:
          type: string
        title:
          type: string
        description:
          type: string
        tags:
          type: [array, "null"]
          items:
            type: string
        buyafeature_cost:
          type: number
        display_order:
          type: integer
        status:
          type: string
    BackupUser:
      type: object
      required: [original_id, name]
      properties:
        original_id:
          type: string
        name:
          type: string
        email:
          type: string
    BackupScore:
      type: object
      required: [original_id, initiative_original_id, user_original_id]
      properties:
        original_id:
          type: string
        initiative_original_id:
          type: string
        user_original_id:
          type: string
        ice_impact:
          type: integer
        ice_confidence:
          type: integer
        ice_ease:
          type: integer
        rice_reach:
          type: integer
        rice_impact:
          type: integer
        rice_confidence:
          type: integer
        rice_effort:
          type: integer
        moscow_category:
          type: string
        buyafeature_purchased:
          type: boolean
        buyafeature_amount_invested:
          type: number
        wsjf_business_value:
          type: integer
        wsjf_time_criticality:
          type: integer
        wsjf_risk_reduction:
          type: integer
        wsjf_job_size:
          type: integer
        pairedcomparison_rank:
          type: integer
        weighted_score:
          type: number
        calculated_score:
          type: number
    BackupComment:
      type: object
      required: [initiative_original_id, is_manager, content]
      properties:
        user_original_id:
          type: string
        initiative_original_id:
          type: string
        is_manager:
          type: boolean
        content:
          type: string
    BackupPairedVote:
      type: object
      required:
        - user_original_id
        - round_number
        - initiative_a_original_id
        - initiative_b_original_id
        - winner_original_id
      properties:
        user_original_id:
          type: string
        round_number:
          type: integer
        initiative_a_original_id:
          type: string
        initiative_b_original_id:
          type: string
        winner_original_id:
          type: string
    BackupWeightedCriterion:
      type: object
      required: [original_id, name, weight, display_order, min_value, max_value]
      properties:
        original_id:
          type: string
        name:
          type: string
        weight:
          type: number
        display_order:
          type: integer
        min_value:
          type: integer
        max_value:
          type: integer
        description:
          type: string
    BackupWeightedScore:
      type: object
      required: [score_original_id, criterion_original_id, value]
      properties:
        score_original_id:
          type: string
        criterion_original_id:
          type: string
        value:
          type: integer
    CreateTableResponse:
      type: object
      description: Returned by the retro export endpoint when it creates a new prioritization table.
      required: [table_id, manager_key, manager_url]
      properties:
        table_id:
          type: string
          format: uuid
        manager_key:
          type: string
          format: uuid
        manager_url:
          type: string
    ExportToExistingResponse:
      type: object
      required: [manager_key]
      properties:
        manager_key:
          type: string
          format: uuid
          description: manager_key of the existing prioritization table the cards were imported into.
    RetroColumnResponse:
      type: object
      required: [id, name, display_order]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        display_order:
          type: integer
        color:
          type: string
    RetroBoardResponse:
      type: object
      required:
        - id
        - name
        - board_type
        - votes_per_user
        - is_active
        - has_voter_password
        - created_at
        - updated_at
        - columns
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        description:
          type: string
        board_type:
          type: string
          enum: [BASIC, STARFISH, FOURLS, CUSTOM]
        votes_per_user:
          type: integer
        is_active:
          type: boolean
        has_voter_password:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
        columns:
          type: array
          items:
            $ref: "#/components/schemas/RetroColumnResponse"
    UpdateRetroBoardRequest:
      type: object
      description: All fields optional; only supplied fields are updated.
      properties:
        name:
          type: string
          minLength: 3
          maxLength: 255
        description:
          type: string
        is_active:
          type: boolean
        votes_per_user:
          type: integer
          minimum: 1
          maximum: 50
    SetRetroVoterPasswordRequest:
      type: object
      required: [password]
      properties:
        password:
          type: string
          minLength: 6
    RetroUserResponse:
      type: object
      required: [id, name, access_token, voter_url, votes_used, created_at]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        access_token:
          type: string
          format: uuid
        voter_url:
          type: string
        votes_used:
          type: integer
        created_at:
          type: string
          format: date-time
    CreateRetroUserRequest:
      type: object
      required: [name]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 255
    RetroCardResponse:
      type: object
      required: [id, column_id, content, vote_count, has_voted, created_at]
      properties:
        id:
          type: string
          format: uuid
        column_id:
          type: string
          format: uuid
        content:
          type: string
        vote_count:
          type: integer
        has_voted:
          type: boolean
          description: Always false in this token/manager context; meaningful only for a voter session.
        created_at:
          type: string
          format: date-time
    MergeRetroCardRequest:
      type: object
      required: [target_card_id]
      properties:
        target_card_id:
          type: string
          format: uuid
          description: The card that the {id} card is merged into.
    RetroResultsResponse:
      type: object
      required:
        - board_name
        - board_type
        - total_voters
        - total_cards
        - total_votes
        - columns
        - participation
      properties:
        board_name:
          type: string
        board_type:
          type: string
        total_voters:
          type: integer
        total_cards:
          type: integer
        total_votes:
          type: integer
        columns:
          type: array
          items:
            $ref: "#/components/schemas/RetroColumnResultResponse"
        participation:
          type: array
          items:
            $ref: "#/components/schemas/RetroParticipationResponse"
    RetroColumnResultResponse:
      type: object
      required: [id, name, cards]
      properties:
        id:
          type: string
          format: uuid
        name:
          type: string
        color:
          type: string
        cards:
          type: array
          description: Sorted by vote_count descending.
          items:
            $ref: "#/components/schemas/RetroCardResponse"
    RetroParticipationResponse:
      type: object
      required: [user_id, user_name, votes_used]
      properties:
        user_id:
          type: string
          format: uuid
        user_name:
          type: string
        votes_used:
          type: integer
    ExportRetroCardsRequest:
      type: object
      required: [card_ids, methodology, table_name, manager_password]
      properties:
        card_ids:
          type: array
          minItems: 1
          items:
            type: string
            format: uuid
        methodology:
          type: string
          enum: [ICE, RICE, MOSCOW, BUYAFEATURE, WSJF, PAIREDCOMPARISON, WEIGHTED]
        table_name:
          type: string
          minLength: 3
          maxLength: 255
        manager_password:
          type: string
          minLength: 6
          description: Password for the brand-new prioritization table that gets created.
        edits:
          type: object
          additionalProperties:
            type: string
          description: card_id -> edited title.
        include_users:
          type: boolean
          description: Also copy the board's participants in as voters on the new table.
    ExportRetroCardsToExistingRequest:
      type: object
      required: [card_ids, target_manager_key, target_password]
      properties:
        card_ids:
          type: array
          minItems: 1
          items:
            type: string
            format: uuid
        target_manager_key:
          type: string
          format: uuid
          description: manager_key of the existing prioritization table to import into.
        target_password:
          type: string
          description: Manager password of the existing prioritization table.
        edits:
          type: object
          additionalProperties:
            type: string
          description: card_id -> edited title.
