# =============================================================================
# MinMaxHub API — OpenAPI 3.1.0 Specification
# =============================================================================
openapi: "3.1.0"
info:
  title: MinMaxHub API
  version: "0.1.0"
  description: >
    REST API for MinMaxHub — a collaborative game-build encyclopedia.
    The Read path serves consensus content via SSR. The Write path accepts
    suggestions and votes for democratic content curation. All responses
    use a standard envelope with traceId, status, message, and data fields.

servers:
  - url: "{apiBaseUrl}"
    description: API Gateway (set NEXT_PUBLIC_API_BASE)
    variables:
      apiBaseUrl:
        default: https://api.example.com

# =============================================================================
# Tags
# =============================================================================
tags:
  - name: Consensus
    description: >
      Approved content derived from voting (Read path). Computes winners,
      serves SSR page data, and provides moderation tools.
  - name: Entities
    description: >
      Content entries within nodes — spells, classes, monsters, items, etc.
      Each entity belongs to exactly one node.
  - name: Media
    description: Image and file upload pipeline via S3 presigned URLs.
  - name: Nodes
    description: >
      Taxonomy tree structure — categories, subcategories, and leaf nodes.
      Nodes form a hierarchical tree identified by slug paths.
  - name: Rules
    description: >
      JSON Schema content validation rules stored in S3. Rules define
      the structure and constraints for suggestion values.
  - name: Suggestions
    description: >
      User-submitted content proposals (Write path). Each suggestion targets
      a specific topic within an entity and carries a value payload.
  - name: Users
    description: User profiles and account information. Email is never exposed publicly.
  - name: Votes
    description: >
      Community voting on suggestions (Write path). Votes are +1 (upvote),
      0 (remove), or -1 (downvote).

# =============================================================================
# Paths — Consensus
# =============================================================================
paths:
  /consensus/compute/{entityId}:
    post:
      summary: Trigger consensus recomputation for an entity
      description: >
        Recomputes winning suggestions for every topic target belonging to
        the specified entity. Returns a summary of how many targets were
        processed and how many winners changed. Requires authentication.
      operationId: computeConsensus
      tags:
        - Consensus
      parameters:
        - name: entityId
          in: path
          required: true
          description: Unique identifier of the entity to recompute consensus for
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        "200":
          description: Consensus recomputed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ComputeConsensusResult"
              example:
                traceId: "req_abc123"
                status: "OK"
                message: "Consensus computed"
                data:
                  entityId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  targetsProcessed: 12
                  winnersUpdated: 3
                  computedAt: "2026-02-15T10:30:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - cognitoAuth: []

  /consensus/moderate/{suggestionId}:
    post:
      summary: Approve or reject a suggestion via moderation
      description: >
        Allows a moderator to approve or reject a pending suggestion.
        Approved suggestions are merged into consensus. Rejected suggestions
        are marked accordingly. An optional reason can be provided.
        Requires authentication.
      operationId: moderateSuggestion
      tags:
        - Consensus
      parameters:
        - name: suggestionId
          in: path
          required: true
          description: Unique identifier of the suggestion to moderate
          schema:
            type: string
            format: uuid
            example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ModerateSuggestionInput"
            example:
              action: "approve"
              reason: "Content is accurate and well-formatted"
      responses:
        "200":
          description: Suggestion moderated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ModerateSuggestionResult"
              example:
                traceId: "req_def456"
                status: "OK"
                message: "Suggestion moderated"
                data:
                  suggestionId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
                  action: "approve"
                  status: "merged"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /consensus/moderation-queue:
    get:
      summary: List suggestions pending moderation
      description: >
        Returns a paginated list of consensus items that have pending
        suggestions awaiting moderator review. Each item includes the
        current consensus and a count of alternative suggestions.
        Requires authentication.
      operationId: getModerationQueue
      tags:
        - Consensus
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 20
        - name: nextToken
          in: query
          required: false
          description: Opaque cursor from a previous response for pagination
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoiY29ucyJ9fQ=="
      responses:
        "200":
          description: Moderation queue retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ModerationQueueResult"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - cognitoAuth: []

  /consensus/page/{entityId}:
    get:
      summary: Get consensus page data for SSR rendering
      description: >
        Returns the full consensus page for an entity, including the entity
        metadata, layout templates, and all computed consensus values. Used
        by the Next.js SSR layer to render public content pages. No
        authentication required.
      operationId: getConsensusPage
      tags:
        - Consensus
      parameters:
        - name: entityId
          in: path
          required: true
          description: Unique identifier of the entity
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        "200":
          description: Consensus page data retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ConsensusPageData"
        "404":
          $ref: "#/components/responses/NotFound"

  /consensus/resolve/{slugPath}:
    get:
      summary: Resolve a full page by slug path
      description: >
        Single-call page resolver for public SSR. Resolves an entity by its
        full slug path and returns entity metadata, layout, consensus values,
        and breadcrumb navigation. The slugPath parameter captures the full
        remaining path (e.g., "dnd/classes/bard/college-of-lore"). No
        authentication required.
      operationId: resolvePageBySlug
      tags:
        - Consensus
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path to resolve. Captures multiple path segments
            (e.g., "dnd/classes/bard/college-of-lore").
          schema:
            type: string
            example: "dnd/classes/bard/college-of-lore"
      responses:
        "200":
          description: Page resolved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/ResolvePageData"
              example:
                traceId: "req_ghi789"
                status: "OK"
                message: "Page resolved"
                data:
                  entity:
                    entityId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    nodeId: "c3d4e5f6-a7b8-9012-cdef-123456789012"
                    title: "College of Lore"
                    slug: "college-of-lore"
                    slugPath: "dnd/classes/bard/college-of-lore"
                  layout: []
                  consensus: {}
                  breadcrumbs:
                    - label: "D&D"
                      href: "/dnd"
                    - label: "Classes"
                      href: "/dnd/classes"
                    - label: "Bard"
                      href: "/dnd/classes/bard"
                  computedAt: "2026-02-15T10:30:00Z"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  # ===========================================================================
  # Paths — Entities
  # ===========================================================================
  /entities:
    post:
      summary: Create a new entity under a node
      description: >
        Creates a content entity under the specified node. The slug must be
        unique within the node's scope and follow the lowercase-kebab-case
        pattern. The entity inherits its layout from the parent node's
        templates. Requires authentication.
      operationId: createEntity
      tags:
        - Entities
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateEntityInput"
            example:
              nodeId: "c3d4e5f6-a7b8-9012-cdef-123456789012"
              slug: "fireball"
              title: "Fireball"
              status: "draft"
      responses:
        "201":
          description: Entity created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Entity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /entities/{id}:
    get:
      summary: Get an entity by its unique identifier
      description: >
        Retrieves a single entity by ID. Returns the full entity object
        including layout templates and metadata. No authentication required.
      operationId: getEntityById
      tags:
        - Entities
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the entity
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        "200":
          description: Entity retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Entity"
        "404":
          $ref: "#/components/responses/NotFound"

    put:
      summary: Update an entity's title and status
      description: >
        Updates the title and status of an existing entity. The entityId
        is taken from the URL path, not the request body. Requires
        authentication.
      operationId: updateEntity
      tags:
        - Entities
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the entity to update
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateEntityInput"
            example:
              title: "Fireball (Revised)"
              status: "published"
      responses:
        "201":
          description: Entity updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Entity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

    delete:
      summary: Delete an entity
      description: >
        Soft-deletes an entity by its unique identifier. Associated
        suggestions and consensus data remain in the database but
        become orphaned. Requires authentication.
      operationId: deleteEntity
      tags:
        - Entities
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the entity to delete
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        "200":
          description: Entity deleted successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: "null"
              example:
                traceId: "req_del001"
                status: "OK"
                message: "Entity deleted"
                data: null
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /entities/{id}/layout:
    patch:
      summary: Refresh an entity's layout from its parent node templates
      description: >
        Re-syncs the entity's layout configuration with the current
        templates defined on its parent node. Returns whether the layout
        was actually changed and the new config version. Requires
        authentication.
      operationId: refreshEntityLayout
      tags:
        - Entities
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the entity
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
      responses:
        "200":
          description: Layout refreshed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/RefreshLayoutResult"
              example:
                traceId: "req_lay001"
                status: "OK"
                message: "Layout refreshed"
                data:
                  entityId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  refreshed: true
                  configVersion: 3
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /entities/node/{id}:
    get:
      summary: List entities belonging to a node
      description: >
        Returns a paginated list of entities under the specified node.
        Optionally filter by entity status. No authentication required.
      operationId: listEntitiesByNode
      tags:
        - Entities
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the parent node
          schema:
            type: string
            format: uuid
            example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        - name: status
          in: query
          required: false
          description: Filter entities by status
          schema:
            type: string
            enum:
              - draft
              - published
              - archived
            example: "published"
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return per page
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
            example: 20
        - name: nextToken
          in: query
          required: false
          description: Opaque cursor from a previous response for pagination
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoiZW50In19"
      responses:
        "200":
          description: Entities retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/EntityPage"

  /entities/path/{slugPath}:
    get:
      summary: Get an entity by its full slug path
      description: >
        Resolves an entity using its full hierarchical slug path. The
        slugPath parameter captures multiple path segments (e.g.,
        "dnd/classes/bard/fireball"). No authentication required.
      operationId: getEntityBySlugPath
      tags:
        - Entities
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path to the entity. Captures multiple segments
            (e.g., "dnd/classes/bard/fireball").
          schema:
            type: string
            example: "dnd/classes/bard/fireball"
      responses:
        "200":
          description: Entity retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Entity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"

  /entities/slugs:
    get:
      summary: List all entity slug paths
      description: >
        Returns a flat array of all entity slug paths in the system.
        Used for sitemap generation and client-side search indexing.
        No authentication required.
      operationId: listEntitySlugs
      tags:
        - Entities
      responses:
        "200":
          description: Slug list retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          type: string
                        description: Array of all entity slug paths
              example:
                traceId: "req_slug001"
                status: "OK"
                message: "Slugs retrieved"
                data:
                  - "dnd/classes/bard/fireball"
                  - "dnd/races/tiefling"
                  - "dnd/monsters/beholder"

  # ===========================================================================
  # Paths — Media
  # ===========================================================================
  /media/presign:
    post:
      summary: Get a presigned URL for image upload
      description: >
        Generates an S3 presigned URL for uploading an image. The image
        is associated with a specific entity and topic. Accepted content
        types are JPEG, PNG, and WebP. Returns both the upload URL and
        the final public image URL. Requires authentication.
      operationId: presignMediaUpload
      tags:
        - Media
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PresignRequest"
            example:
              entityId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              topicKey: "hero-image"
              contentType: "image/webp"
      responses:
        "201":
          description: Presigned URL generated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/PresignResponse"
              example:
                traceId: "req_media001"
                status: "CREATED"
                message: "Presigned URL generated"
                data:
                  uploadUrl: "https://s3.amazonaws.com/bucket/presigned..."
                  imageUrl: "https://cdn.example.com/images/abc123.webp"
                  uploadId: "upl_abc123"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  # ===========================================================================
  # Paths — Nodes
  # ===========================================================================
  /nodes:
    post:
      summary: Create a new node in the taxonomy tree
      description: >
        Creates a node under the specified parent. The slug must be unique
        among siblings. Provide either parentId or parentSlugPath to identify
        the parent node. Omit both to create a root-level node. Requires
        authentication.
      operationId: createNode
      tags:
        - Nodes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateNodeInput"
            example:
              parentSlugPath: "dnd/classes"
              slug: "bard"
              title: "Bard"
              nodeKind: "entity"
      responses:
        "201":
          description: Node created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Node"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /nodes/{id}:
    get:
      summary: Get a node by its unique identifier
      description: >
        Retrieves a single node by ID, including its full metadata,
        templates, and position in the tree. Requires authentication.
      operationId: getNodeById
      tags:
        - Nodes
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the node
          schema:
            type: string
            format: uuid
            example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
      responses:
        "200":
          description: Node retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Node"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /nodes/{id}/archive:
    patch:
      summary: Archive a node
      description: >
        Marks a node as archived. Archiving is blocked if the node has
        child nodes (childCount > 0) to prevent orphaning. Requires
        authentication.
      operationId: archiveNode
      tags:
        - Nodes
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the node to archive
          schema:
            type: string
            format: uuid
            example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
      responses:
        "204":
          description: Node archived successfully (no content)
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: >
            Forbidden — node has child nodes and cannot be archived
            (orphan prevention)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /nodes/{id}/templates:
    put:
      summary: Update a node's layout templates
      description: >
        Replaces the layout templates for a node. Templates define the
        structure that child entities inherit for their content layout.
        Each template specifies a topic key, validation rule, display
        order, and optional collection items. Requires authentication.
      operationId: updateNodeTemplates
      tags:
        - Nodes
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the node
          schema:
            type: string
            format: uuid
            example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateNodeTemplatesInput"
            example:
              nodeId: "c3d4e5f6-a7b8-9012-cdef-123456789012"
              templates:
                - order: 0
                  rule: "SuperText"
                  kind: "single"
                  title: "Description"
                  topicKey: "description"
                - order: 1
                  rule: "StatBlock"
                  kind: "collections"
                  collectionMode: "expandable"
                  title: "Abilities"
                  topicKey: "abilities"
                  items:
                    - itemKey: "spellcasting"
                      label: "Spellcasting"
      responses:
        "200":
          description: Templates updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Node"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /nodes/ancestors/{slugPath}:
    get:
      summary: List ancestor nodes for a slug path
      description: >
        Returns all ancestor nodes from root to the node identified by the
        slug path, ordered from root to leaf. Used for breadcrumb navigation.
        The slugPath parameter captures multiple segments. Requires
        authentication.
      operationId: listNodeAncestors
      tags:
        - Nodes
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path. Captures multiple segments
            (e.g., "dnd/classes/bard").
          schema:
            type: string
            example: "dnd/classes/bard"
      responses:
        "200":
          description: Ancestors retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/Node"
      security:
        - cognitoAuth: []

  /nodes/children/{slugPath}:
    get:
      summary: List direct children of a node
      description: >
        Returns a paginated list of direct child nodes under the node
        identified by the slug path. The slugPath captures multiple
        segments. Pass "ROOT" to list root-level nodes. Requires
        authentication.
      operationId: listNodeChildren
      tags:
        - Nodes
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path of the parent node. Captures multiple segments.
            Use "ROOT" for top-level nodes.
          schema:
            type: string
            example: "dnd/classes"
        - name: limit
          in: query
          required: false
          description: Maximum number of children to return
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
            example: 50
      responses:
        "200":
          description: Children retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/NodePage"
      security:
        - cognitoAuth: []

  /nodes/descendants/{slugPath}:
    get:
      summary: List all descendants of a node
      description: >
        Returns a paginated list of all descendant nodes in the subtree
        rooted at the node identified by the slug path. The slugPath
        captures multiple segments. Requires authentication.
      operationId: listNodeDescendants
      tags:
        - Nodes
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path of the root node. Captures multiple segments
            (e.g., "dnd/classes").
          schema:
            type: string
            example: "dnd/classes"
        - name: limit
          in: query
          required: false
          description: Maximum number of descendants to return
          schema:
            type: integer
            minimum: 1
            maximum: 1000
            default: 100
            example: 50
      responses:
        "200":
          description: Descendants retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/NodePage"
      security:
        - cognitoAuth: []

  /nodes/path/{slugPath}:
    get:
      summary: Get a node by its slug path
      description: >
        Resolves a node using its full hierarchical slug path. The slugPath
        parameter captures multiple segments (e.g., "dnd/classes/bard").
        Requires authentication.
      operationId: getNodeBySlugPath
      tags:
        - Nodes
      parameters:
        - name: slugPath
          in: path
          required: true
          description: >
            Full slug path of the node. Captures multiple segments
            (e.g., "dnd/classes/bard").
          schema:
            type: string
            example: "dnd/classes/bard"
      responses:
        "200":
          description: Node retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Node"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  # ===========================================================================
  # Paths — Rules
  # ===========================================================================
  /rules/{folder}:
    get:
      summary: List all rules in a folder
      description: >
        Returns a list of rule summaries for the specified folder. Each
        summary includes metadata like the component key, deprecation
        status, and creator info. Requires authentication.
      operationId: listFolderRules
      tags:
        - Rules
      parameters:
        - name: folder
          in: path
          required: true
          description: Rule folder name (e.g., "layout", "validation")
          schema:
            type: string
            minLength: 1
            example: "layout"
      responses:
        "200":
          description: Rules listed successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        type: array
                        items:
                          $ref: "#/components/schemas/RuleSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - cognitoAuth: []

  /rules/{folder}/{componentKey}:
    get:
      summary: Get a single rule by folder and component key
      description: >
        Retrieves the full rule definition including its JSON Schema
        body. Returns an ETag header for optimistic concurrency on
        subsequent updates. Requires authentication.
      operationId: getRule
      tags:
        - Rules
      parameters:
        - name: folder
          in: path
          required: true
          description: Rule folder name
          schema:
            type: string
            minLength: 1
            example: "layout"
        - name: componentKey
          in: path
          required: true
          description: Component key identifying the rule within the folder
          schema:
            type: string
            minLength: 1
            example: "SuperText"
      responses:
        "200":
          description: Rule retrieved successfully
          headers:
            ETag:
              description: Entity tag for optimistic concurrency control
              schema:
                type: string
                example: '"a1b2c3d4"'
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/RuleEntity"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

    post:
      summary: Create a new rule
      description: >
        Creates a new rule under the specified folder and component key.
        The schemaJson field must be a valid JSON string representing
        the rule's JSON Schema. Set overwrite=true to replace an existing
        rule. Requires authentication.
      operationId: createRule
      tags:
        - Rules
      parameters:
        - name: folder
          in: path
          required: true
          description: Rule folder name
          schema:
            type: string
            minLength: 1
            example: "layout"
        - name: componentKey
          in: path
          required: true
          description: Component key for the new rule
          schema:
            type: string
            minLength: 1
            example: "SuperText"
        - name: overwrite
          in: query
          required: false
          description: Set to true to overwrite an existing rule
          schema:
            type: boolean
            default: false
            example: false
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRuleBody"
            example:
              schemaJson: '{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string"}}}'
      responses:
        "201":
          description: Rule created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/RuleEntity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

    put:
      summary: Update an existing rule
      description: >
        Updates a rule's JSON Schema body. Supports optimistic concurrency
        via the If-Match header (ETag from a previous GET). Set
        overwrite=true to bypass the concurrency check. Requires
        authentication.
      operationId: updateRule
      tags:
        - Rules
      parameters:
        - name: folder
          in: path
          required: true
          description: Rule folder name
          schema:
            type: string
            minLength: 1
            example: "layout"
        - name: componentKey
          in: path
          required: true
          description: Component key of the rule to update
          schema:
            type: string
            minLength: 1
            example: "SuperText"
        - name: overwrite
          in: query
          required: false
          description: Set to true to bypass ETag concurrency check
          schema:
            type: boolean
            default: false
            example: false
        - name: If-Match
          in: header
          required: false
          description: ETag from a previous GET for optimistic concurrency
          schema:
            type: string
            example: '"a1b2c3d4"'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateRuleBody"
            example:
              schemaJson: '{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string","minLength":1}}}'
      responses:
        "200":
          description: Rule updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/RuleEntity"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          $ref: "#/components/responses/Conflict"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  # ===========================================================================
  # Paths — Suggestions
  # ===========================================================================
  /suggestions:
    post:
      summary: Create a new content suggestion
      description: >
        Submits a content suggestion for a specific topic within an entity.
        The value payload is validated against the topic's JSON Schema rule
        (schema-on-write). The suggestion enters the Write path for community
        voting. Requires authentication.
      operationId: createSuggestion
      tags:
        - Suggestions
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateSuggestionInput"
            example:
              entityId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              topicKey: "description"
              operation: "SET"
              value:
                text: "A bard who collects lore and stories from across the realms."
      responses:
        "201":
          description: Suggestion created successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Suggestion"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /suggestions/{id}:
    get:
      summary: Get a suggestion by its unique identifier
      description: >
        Retrieves a single suggestion including its value payload, vote
        counts, and metadata. No authentication required.
      operationId: getSuggestionById
      tags:
        - Suggestions
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the suggestion
          schema:
            type: string
            format: uuid
            example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        "200":
          description: Suggestion retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Suggestion"
        "404":
          $ref: "#/components/responses/NotFound"

    patch:
      summary: Update a suggestion's value or status
      description: >
        Updates an existing suggestion. Only the original author can update
        the value. Moderators can change the status via consensus moderation.
        The suggestionId is taken from the URL path, not the request body.
        Requires authentication.
      operationId: updateSuggestion
      tags:
        - Suggestions
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the suggestion to update
          schema:
            type: string
            format: uuid
            example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateSuggestionInput"
            example:
              value:
                text: "A bard who weaves magic through music and storytelling."
      responses:
        "200":
          description: Suggestion updated successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/Suggestion"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

    delete:
      summary: Soft-delete a suggestion
      description: >
        Marks a suggestion as deleted. Only the original author can delete
        their own suggestions. The suggestion remains in the database but
        is excluded from consensus computation. Requires authentication.
      operationId: deleteSuggestion
      tags:
        - Suggestions
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the suggestion to delete
          schema:
            type: string
            format: uuid
            example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
      responses:
        "200":
          description: Suggestion deleted successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data: {}
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /suggestions/author/{id}:
    get:
      summary: List suggestions by a specific author
      description: >
        Returns a paginated list of suggestions created by the specified
        author. Requires authentication.
      operationId: listSuggestionsByAuthor
      tags:
        - Suggestions
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the author (userId)
          schema:
            type: string
            format: uuid
            example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 20
        - name: nextToken
          in: query
          required: false
          description: Opaque cursor from a previous response for pagination
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoic3VnIn19"
      responses:
        "200":
          description: Suggestions retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/SuggestionPage"
      security:
        - cognitoAuth: []

  /suggestions/entity/{id}:
    get:
      summary: List suggestions for a specific entity
      description: >
        Returns a paginated list of suggestions targeting the specified
        entity, across all topics. No authentication required.
      operationId: listSuggestionsByEntity
      tags:
        - Suggestions
      parameters:
        - name: id
          in: path
          required: true
          description: Unique identifier of the entity
          schema:
            type: string
            format: uuid
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 20
        - name: nextToken
          in: query
          required: false
          description: Opaque cursor from a previous response for pagination
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoic3VnIn19"
      responses:
        "200":
          description: Suggestions retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/SuggestionPage"

  /suggestions/target:
    get:
      summary: List suggestions by target (entity + topic + item)
      description: >
        Returns a paginated list of suggestions filtered by entity, topic
        key, and optionally item key. Used to show all competing suggestions
        for a specific content slot. Requires authentication.
      operationId: listSuggestionsByTarget
      tags:
        - Suggestions
      parameters:
        - name: entityId
          in: query
          required: true
          description: Entity to filter suggestions for
          schema:
            type: string
            minLength: 1
            example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        - name: topicKey
          in: query
          required: true
          description: Topic key to filter suggestions for
          schema:
            type: string
            minLength: 1
            example: "description"
        - name: itemKey
          in: query
          required: false
          description: Optional item key for collection-type topics
          schema:
            type: string
            minLength: 1
            example: "spellcasting"
        - name: limit
          in: query
          required: false
          description: Maximum number of items to return per page
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 20
        - name: nextToken
          in: query
          required: false
          description: Opaque cursor from a previous response for pagination
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoic3VnIn19"
      responses:
        "200":
          description: Suggestions retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/SuggestionPage"
      security:
        - cognitoAuth: []

  # ===========================================================================
  # Paths — Users
  # ===========================================================================
  /users:
    get:
      summary: List all users
      description: >
        Returns a paginated list of user profiles. Email is never included
        in the response. Requires authentication.
      operationId: listUsers
      tags:
        - Users
      parameters:
        - name: limit
          in: query
          required: false
          description: Maximum number of users to return per page
          schema:
            type: integer
            minimum: 1
            default: 20
            example: 20
        - name: lastKey
          in: query
          required: false
          description: Encoded pagination key from a previous response
          schema:
            type: string
            example: "eyJsYXN0S2V5Ijp7InBrIjoidXNyIn19"
      responses:
        "200":
          description: Users retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/UserPage"
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - cognitoAuth: []

  /users/{userId}:
    get:
      summary: Get a user profile by ID
      description: >
        Retrieves a user's public profile by their unique identifier.
        Email is omitted from the response. Requires authentication.
      operationId: getUserById
      tags:
        - Users
      parameters:
        - name: userId
          in: path
          required: true
          description: Unique identifier of the user
          schema:
            type: string
            format: uuid
            example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
      responses:
        "200":
          description: User profile retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/UserProfile"
              example:
                traceId: "req_usr001"
                status: "OK"
                message: "User retrieved"
                data:
                  userId: "d4e5f6a7-b8c9-0123-def0-123456789abc"
                  username: "dungeon_master_42"
                  photoUrl: "https://cdn.example.com/avatars/dm42.webp"
                  rank: "ADEPT"
                  bio: "Forever DM. Collector of mimics and bad puns."
                  isActive: true
                  createdAt: "2025-06-01T08:00:00Z"
                  updatedAt: "2026-02-10T14:30:00Z"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  /users/username/{username}:
    get:
      summary: Get a user profile by username
      description: >
        Retrieves a user's public profile by their username. Email is
        omitted from the response. Requires authentication.
      operationId: getUserByUsername
      tags:
        - Users
      parameters:
        - name: username
          in: path
          required: true
          description: Username of the user
          schema:
            type: string
            example: "dungeon_master_42"
      responses:
        "200":
          description: User profile retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/UserProfile"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
      security:
        - cognitoAuth: []

  # ===========================================================================
  # Paths — Votes
  # ===========================================================================
  /votes:
    post:
      summary: Cast or change a vote on a suggestion
      description: >
        Casts a vote on a suggestion. Value 1 is an upvote, -1 is a
        downvote, and 0 removes a previous vote. Each user can have at
        most one active vote per suggestion. Returns the updated vote
        tallies (score, upvotes, downvotes). Requires authentication.
      operationId: castVote
      tags:
        - Votes
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CastVoteInput"
            example:
              suggestionId: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
              value: 1
      responses:
        "200":
          description: Vote recorded successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/VoteDelta"
              example:
                traceId: "req_vote001"
                status: "OK"
                message: "Vote recorded"
                data:
                  score: 7
                  upvotes: 9
                  downvotes: 2
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "422":
          $ref: "#/components/responses/UnprocessableEntity"
      security:
        - cognitoAuth: []

  /votes/me:
    get:
      summary: Get the authenticated user's votes
      description: >
        Returns the current user's votes, optionally filtered by a
        comma-separated list of suggestion IDs. Returns a map of
        suggestionId to vote value. Requires authentication.
      operationId: getMyVotes
      tags:
        - Votes
      parameters:
        - name: suggestionIds
          in: query
          required: false
          description: >
            Comma-separated list of suggestion IDs to filter votes for.
            Omit to return all votes.
          schema:
            type: string
            example: "b2c3d4e5-f6a7-8901-bcde-f12345678901,c3d4e5f6-a7b8-9012-cdef-234567890abc"
      responses:
        "200":
          description: Votes retrieved successfully
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/ApiEnvelope"
                  - type: object
                    properties:
                      data:
                        $ref: "#/components/schemas/VoteMap"
              example:
                traceId: "req_myvotes001"
                status: "OK"
                message: "Votes retrieved"
                data:
                  votes:
                    b2c3d4e5-f6a7-8901-bcde-f12345678901: 1
                    c3d4e5f6-a7b8-9012-cdef-234567890abc: -1
        "401":
          $ref: "#/components/responses/Unauthorized"
      security:
        - cognitoAuth: []

# =============================================================================
# Components
# =============================================================================
components:
  # ---------------------------------------------------------------------------
  # Reusable Responses
  # ---------------------------------------------------------------------------
  responses:
    BadRequest:
      description: Invalid input — missing required fields or malformed data
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            traceId: "req_err400"
            status: "BAD_REQUEST"
            message: "Invalid input"

    Unauthorized:
      description: Missing or invalid authentication token
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            traceId: "req_err401"
            status: "UNAUTHORIZED"
            message: "Authentication required"

    NotFound:
      description: Resource not found — no record exists with the given identifier
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            traceId: "req_err404"
            status: "NOT_FOUND"
            message: "Resource not found"

    Conflict:
      description: Conflict — a resource with the same unique key already exists
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            traceId: "req_err409"
            status: "CONFLICT"
            message: "Resource already exists"

    UnprocessableEntity:
      description: Validation failed — request body does not match the expected schema
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
          example:
            traceId: "req_err422"
            status: "UNPROCESSABLE_ENTITY"
            message: "Validation failed"

  # ---------------------------------------------------------------------------
  # Schemas — Shared
  # ---------------------------------------------------------------------------
  schemas:
    ApiEnvelope:
      type: object
      description: >
        Standard response envelope wrapping all API responses. Every
        response includes a trace ID for debugging, a machine-readable
        status, and a human-readable message.
      required:
        - status
        - message
      properties:
        traceId:
          type: string
          description: Unique request trace ID for debugging and log correlation
          example: "req_abc123"
        status:
          type: string
          description: Machine-readable status code
          enum:
            - OK
            - CREATED
            - ACCEPTED
            - NO_CONTENT
            - BAD_REQUEST
            - UNAUTHORIZED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - UNPROCESSABLE_ENTITY
            - ERROR
          example: "OK"
        message:
          type: string
          description: Human-readable status message
          example: "Success"

    Error:
      type: object
      description: >
        Standard error response envelope. All API errors follow this shape.
        The status field maps to AppError codes in the Lambda layer.
      required:
        - status
        - message
      properties:
        traceId:
          type: string
          description: Request trace ID for debugging and log correlation
          example: "req_err001"
        status:
          type: string
          description: Machine-readable error code
          enum:
            - BAD_REQUEST
            - UNAUTHORIZED
            - FORBIDDEN
            - NOT_FOUND
            - CONFLICT
            - UNPROCESSABLE_ENTITY
            - ERROR
          example: "NOT_FOUND"
        message:
          type: string
          description: Human-readable error description
          example: "Resource not found"

    UserSummary:
      type: object
      description: >
        Lightweight user reference embedded in created/updated metadata.
        Contains only public profile fields — never includes email.
      required:
        - userId
        - username
      properties:
        userId:
          type: string
          format: uuid
          description: Unique identifier of the user
          example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
        username:
          type: string
          description: Display username
          example: "dungeon_master_42"
        photoUrl:
          type:
            - string
            - "null"
          description: URL to the user's avatar image
          example: "https://cdn.example.com/avatars/dm42.webp"

    BreadcrumbItem:
      type: object
      description: A single breadcrumb navigation entry with label and link.
      required:
        - label
        - href
      properties:
        label:
          type: string
          description: Display text for the breadcrumb link
          example: "Classes"
        href:
          type: string
          description: Relative URL path for the breadcrumb
          example: "/dnd/classes"

    CollectionItemDef:
      type: object
      description: >
        Definition for a single item within a collections-type template
        slot. Specifies the item key, display label, and optional defaults.
      required:
        - itemKey
        - label
      properties:
        itemKey:
          type: string
          minLength: 1
          description: Unique key identifying this item within the collection
          example: "spellcasting"
        label:
          type: string
          minLength: 1
          description: Human-readable label for this collection item
          example: "Spellcasting"
        defaultValue:
          type: object
          additionalProperties: true
          description: Default field values pre-populated for new suggestions
          example:
            text: ""
        lockedFields:
          type: array
          items:
            type: string
          description: Field keys that cannot be modified by suggestions
          example: ["itemKey"]

    EntityLayoutTemplate:
      type: object
      description: >
        Defines a content slot in an entity's layout. Templates are
        inherited from the parent node and determine what topics
        appear on the entity page.
      required:
        - topicKey
        - component
        - title
        - kind
        - order
      properties:
        topicKey:
          type: string
          minLength: 1
          description: Unique key identifying this topic slot
          example: "description"
        component:
          type: string
          minLength: 1
          description: React component key used to render this template section
          example: "SuperText"
        title:
          type: string
          minLength: 1
          description: Display title for this template section
          example: "Description"
        kind:
          type: string
          enum:
            - single
            - collections
          description: Whether this slot holds a single value or a collection of items
          example: "single"
        order:
          type: integer
          minimum: 0
          description: Display order (lower numbers appear first)
          example: 0
        image:
          type: object
          description: Optional hero image for this template section
          properties:
            url:
              type: string
              format: uri
              description: Image URL
              example: "https://cdn.example.com/images/hero.webp"
            alt:
              type: string
              description: Alt text for the image
              example: "A bard playing a lute"
        collectionMode:
          type: string
          enum:
            - expandable
            - closed
          description: Display mode for collections-type templates
          example: "expandable"
        rule:
          type: string
          minLength: 1
          description: JSON Schema rule key for validating suggestion values
          example: "StatBlock"
        items:
          type: array
          items:
            $ref: "#/components/schemas/CollectionItemDef"
          description: Pre-defined items within a collections-type template

    # -------------------------------------------------------------------------
    # Schemas — Consensus
    # -------------------------------------------------------------------------
    ComputeConsensusResult:
      type: object
      description: Summary of a consensus computation run.
      required:
        - entityId
        - targetsProcessed
        - winnersUpdated
        - computedAt
      properties:
        entityId:
          type: string
          format: uuid
          description: Entity that was recomputed
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        targetsProcessed:
          type: integer
          description: Number of topic targets evaluated
          example: 12
        winnersUpdated:
          type: integer
          description: Number of winners that changed during this computation
          example: 3
        computedAt:
          type: string
          format: date-time
          description: Timestamp of the computation
          example: "2026-02-15T10:30:00Z"

    ConsensusItem:
      type: object
      description: >
        A single consensus record representing the winning suggestion
        for a specific topic target within an entity.
      required:
        - entityId
        - topicKey
        - value
        - confidence
        - upvotes
        - downvotes
        - totalVotes
        - computedAt
        - createdAt
        - updatedAt
      properties:
        entityId:
          type: string
          format: uuid
          description: Entity this consensus belongs to
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        topicKey:
          type: string
          description: Topic key this consensus covers
          example: "description"
        itemKey:
          type:
            - string
            - "null"
          description: Item key for collection-type topics, null for single topics
          example: null
        winningSuggestionId:
          type:
            - string
            - "null"
          format: uuid
          description: ID of the suggestion that won consensus, null if no winner
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        value:
          description: The winning suggestion's value payload
          example:
            text: "A bard who collects lore and stories from across the realms."
        authorId:
          type: string
          description: User ID of the winning suggestion's author
          example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
        authorUsername:
          type: string
          description: Username of the winning suggestion's author
          example: "lore_keeper"
        authorPhotoUrl:
          type: string
          description: Avatar URL of the winning suggestion's author
          example: "https://cdn.example.com/avatars/lk.webp"
        confidence:
          type: number
          description: Confidence score (0-1) based on vote distribution
          example: 0.85
        upvotes:
          type: integer
          description: Total upvotes for the winning suggestion
          example: 15
        downvotes:
          type: integer
          description: Total downvotes for the winning suggestion
          example: 2
        totalVotes:
          type: integer
          description: Total number of votes cast
          example: 17
        computedAt:
          type: string
          format: date-time
          description: When this consensus was last computed
          readOnly: true
          example: "2026-02-15T10:30:00Z"
        createdAt:
          type: string
          format: date-time
          description: When this consensus record was first created
          readOnly: true
          example: "2026-01-20T08:00:00Z"
        updatedAt:
          type: string
          format: date-time
          description: When this consensus record was last updated
          readOnly: true
          example: "2026-02-15T10:30:00Z"

    ConsensusPageEntity:
      type: object
      description: Lightweight entity metadata included in consensus page responses.
      required:
        - entityId
        - nodeId
        - title
        - slug
        - slugPath
      properties:
        entityId:
          type: string
          format: uuid
          description: Unique identifier of the entity
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        nodeId:
          type: string
          format: uuid
          description: Parent node identifier
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        title:
          type: string
          description: Display title
          example: "College of Lore"
        slug:
          type: string
          description: URL-safe slug
          example: "college-of-lore"
        slugPath:
          type: string
          description: Full hierarchical slug path
          example: "dnd/classes/bard/college-of-lore"

    ConsensusPageData:
      type: object
      description: >
        Full consensus page payload for SSR rendering. Contains entity
        metadata, layout configuration, and all computed consensus values.
      required:
        - entity
        - layout
        - consensus
        - computedAt
      properties:
        entity:
          $ref: "#/components/schemas/ConsensusPageEntity"
        layout:
          type: array
          items:
            $ref: "#/components/schemas/EntityLayoutTemplate"
          description: Layout template configuration for this entity
        consensus:
          type: object
          additionalProperties: true
          description: Map of topic keys to their consensus values
          example:
            description:
              text: "A bard who collects lore and stories."
        computedAt:
          type: string
          format: date-time
          description: When the consensus was last computed
          example: "2026-02-15T10:30:00Z"

    ModerateSuggestionInput:
      type: object
      description: Request body for moderating a suggestion.
      required:
        - action
      properties:
        action:
          type: string
          enum:
            - approve
            - reject
          description: Moderation action to take
          example: "approve"
        reason:
          type: string
          description: Optional reason for the moderation decision
          example: "Content is accurate and well-formatted"

    ModerateSuggestionResult:
      type: object
      description: Result of a moderation action on a suggestion.
      required:
        - suggestionId
        - action
        - status
      properties:
        suggestionId:
          type: string
          format: uuid
          description: ID of the moderated suggestion
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        action:
          type: string
          enum:
            - approve
            - reject
          description: The moderation action that was taken
          example: "approve"
        status:
          type: string
          enum:
            - merged
            - rejected
          description: Resulting status of the suggestion
          example: "merged"

    ModerationQueueItem:
      type: object
      description: A single entry in the moderation queue.
      required:
        - consensus
        - alternativesCount
      properties:
        consensus:
          $ref: "#/components/schemas/ConsensusItem"
        alternativesCount:
          type: integer
          description: Number of pending alternative suggestions
          example: 3

    ModerationQueueResult:
      type: object
      description: Paginated moderation queue response.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/ModerationQueueItem"
          description: List of items pending moderation
        nextToken:
          type: string
          description: Opaque cursor for fetching the next page
          example: "eyJsYXN0S2V5Ijp7InBrIjoibW9kIn19"

    ResolvePageData:
      type: object
      description: >
        Extended consensus page data returned by the slug resolver.
        Includes breadcrumb navigation in addition to entity, layout,
        and consensus data.
      required:
        - entity
        - layout
        - consensus
        - breadcrumbs
        - computedAt
      properties:
        entity:
          $ref: "#/components/schemas/ConsensusPageEntity"
        layout:
          type: array
          items:
            $ref: "#/components/schemas/EntityLayoutTemplate"
          description: Layout template configuration for this entity
        consensus:
          type: object
          additionalProperties: true
          description: Map of topic keys to their consensus values
        breadcrumbs:
          type: array
          items:
            $ref: "#/components/schemas/BreadcrumbItem"
          description: Breadcrumb navigation from root to this entity
        computedAt:
          type: string
          format: date-time
          description: When the consensus was last computed
          example: "2026-02-15T10:30:00Z"

    # -------------------------------------------------------------------------
    # Schemas — Entities
    # -------------------------------------------------------------------------
    Entity:
      type: object
      description: >
        A content entry within a node. Entities hold structured content
        defined by their layout templates and populated by suggestions.
      required:
        - entityId
        - nodeId
        - slug
        - slugPath
        - title
        - status
        - layout
        - configVersion
        - createdAt
        - updatedAt
        - createdBy
        - updatedBy
      properties:
        entityId:
          type: string
          format: uuid
          description: Unique identifier, server-generated
          readOnly: true
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        nodeId:
          type: string
          format: uuid
          description: Parent node this entity belongs to
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        slug:
          type: string
          pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
          description: URL-safe identifier, unique within the parent node
          example: "fireball"
        slugPath:
          type: string
          description: Full hierarchical slug path from root
          readOnly: true
          example: "dnd/spells/fireball"
        title:
          type: string
          minLength: 1
          description: Human-readable display title
          example: "Fireball"
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Publication status of the entity
          example: "published"
        layout:
          type: array
          items:
            $ref: "#/components/schemas/EntityLayoutTemplate"
          description: Content layout inherited from parent node templates
        configVersion:
          type: integer
          description: Layout configuration version, incremented on refresh
          readOnly: true
          example: 2
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of creation
          readOnly: true
          example: "2026-01-15T09:00:00Z"
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of last modification
          readOnly: true
          example: "2026-02-10T14:30:00Z"
        createdBy:
          $ref: "#/components/schemas/UserSummary"
        updatedBy:
          $ref: "#/components/schemas/UserSummary"

    EntityPage:
      type: object
      description: Paginated list of entities.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Entity"
          description: Entities in this page
        cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for fetching the next page, null if last page
          example: "eyJsYXN0S2V5Ijp7InBrIjoiZW50In19"

    CreateEntityInput:
      type: object
      description: >
        Request body for creating a new entity. Validated against
        createEntitySchema in contracts.
      required:
        - nodeId
        - slug
        - title
      properties:
        nodeId:
          type: string
          format: uuid
          description: Parent node ID to create the entity under
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        slug:
          type: string
          minLength: 1
          pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
          description: URL-safe identifier, must be unique within the node
          example: "fireball"
        title:
          type: string
          minLength: 1
          description: Human-readable display title
          example: "Fireball"
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          default: "draft"
          description: Initial publication status
          example: "draft"

    UpdateEntityInput:
      type: object
      description: >
        Request body for updating an entity. The entityId is taken
        from the URL path, not the request body.
      required:
        - title
        - status
      properties:
        title:
          type: string
          minLength: 1
          description: New display title
          example: "Fireball (Revised)"
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: New publication status
          example: "published"

    RefreshLayoutResult:
      type: object
      description: Result of refreshing an entity's layout from its parent node.
      required:
        - entityId
        - refreshed
        - configVersion
      properties:
        entityId:
          type: string
          format: uuid
          description: Entity that was refreshed
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        refreshed:
          type: boolean
          description: Whether the layout actually changed
          example: true
        configVersion:
          type: integer
          description: New configuration version after refresh
          example: 3

    # -------------------------------------------------------------------------
    # Schemas — Media
    # -------------------------------------------------------------------------
    PresignRequest:
      type: object
      description: Request body for generating an S3 presigned upload URL.
      required:
        - entityId
        - topicKey
        - contentType
      properties:
        entityId:
          type: string
          minLength: 1
          description: Entity the image is associated with
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        topicKey:
          type: string
          minLength: 1
          description: Topic key the image belongs to
          example: "hero-image"
        contentType:
          type: string
          enum:
            - image/jpeg
            - image/png
            - image/webp
          description: MIME type of the image to upload
          example: "image/webp"
        itemKey:
          type:
            - string
            - "null"
          minLength: 1
          description: Optional item key for collection-type topics
          example: null

    PresignResponse:
      type: object
      description: Presigned URL details for uploading an image to S3.
      required:
        - uploadUrl
        - imageUrl
        - uploadId
      properties:
        uploadUrl:
          type: string
          format: uri
          description: S3 presigned URL for PUT upload
          example: "https://s3.amazonaws.com/bucket/presigned..."
        imageUrl:
          type: string
          format: uri
          description: Final public URL where the image will be accessible
          example: "https://cdn.example.com/images/abc123.webp"
        uploadId:
          type: string
          description: Unique identifier for this upload operation
          example: "upl_abc123"

    # -------------------------------------------------------------------------
    # Schemas — Nodes
    # -------------------------------------------------------------------------
    Node:
      type: object
      description: >
        A node in the taxonomy tree. Nodes form a hierarchical structure
        of categories and subcategories. Leaf nodes of kind "entity" hold
        content entries.
      required:
        - id
        - treeId
        - slug
        - slugPath
        - path
        - title
        - nodeKind
        - status
        - depth
        - childCount
        - createdAt
        - updatedAt
        - createdBy
        - updatedBy
      properties:
        id:
          type: string
          format: uuid
          description: Unique identifier, server-generated
          readOnly: true
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        treeId:
          type: string
          description: Tree partition identifier
          readOnly: true
          example: "MAIN"
        parentId:
          type:
            - string
            - "null"
          format: uuid
          description: Parent node ID, null for root nodes
          readOnly: true
          example: "e5f6a7b8-c9d0-1234-ef01-234567890abc"
        slug:
          type: string
          minLength: 1
          description: URL-safe identifier, unique among siblings
          example: "bard"
        slugPath:
          type: string
          description: Full hierarchical slug path from root
          readOnly: true
          example: "dnd/classes/bard"
        path:
          type: string
          description: Materialized path from root hierarchy
          readOnly: true
          example: "/dnd/classes/bard"
        title:
          type: string
          minLength: 1
          description: Human-readable display title
          example: "Bard"
        nodeKind:
          type: string
          enum:
            - folder
            - entity
          description: Whether this node is a category folder or an entity leaf
          example: "entity"
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Publication status of the node
          example: "published"
        content:
          type:
            - object
            - "null"
          additionalProperties: true
          description: Optional structured content associated with the node
          example: null
        templates:
          type: array
          items:
            $ref: "#/components/schemas/EntityLayoutTemplate"
          description: Layout templates that child entities inherit
        configVersion:
          type: integer
          description: Template configuration version
          readOnly: true
          example: 1
        depth:
          type: integer
          minimum: 0
          description: Depth in the tree (0 = root)
          readOnly: true
          example: 2
        childCount:
          type: integer
          minimum: 0
          description: Number of direct children
          readOnly: true
          example: 5
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of creation
          readOnly: true
          example: "2026-01-10T08:00:00Z"
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of last modification
          readOnly: true
          example: "2026-02-05T12:00:00Z"
        createdBy:
          $ref: "#/components/schemas/UserSummary"
        updatedBy:
          $ref: "#/components/schemas/UserSummary"

    NodePage:
      type: object
      description: Paginated list of nodes.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Node"
          description: Nodes in this page
        cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for fetching the next page, null if last page
          example: "eyJsYXN0S2V5Ijp7InBrIjoibm9kZSJ9fQ=="

    CreateNodeInput:
      type: object
      description: >
        Request body for creating a new node. Provide either parentId
        or parentSlugPath to specify the parent. Omit both for root nodes.
      required:
        - slug
        - title
        - nodeKind
      properties:
        parentSlugPath:
          type: string
          description: Slug path of the parent node (alternative to parentId)
          example: "dnd/classes"
        parentId:
          type: string
          format: uuid
          description: UUID of the parent node (alternative to parentSlugPath)
          example: "e5f6a7b8-c9d0-1234-ef01-234567890abc"
        slug:
          type: string
          minLength: 1
          description: URL-safe identifier, must be unique among siblings
          example: "bard"
        title:
          type: string
          minLength: 1
          description: Human-readable display title
          example: "Bard"
        nodeKind:
          type: string
          enum:
            - folder
            - entity
          description: Whether this node is a category folder or entity leaf
          example: "entity"
        content:
          type:
            - object
            - "null"
          additionalProperties: true
          description: Optional structured content
          example: null
        status:
          type: string
          enum:
            - draft
            - published
            - archived
          description: Initial publication status
          example: "draft"
        sortOrder:
          type: integer
          description: Optional sort order among siblings
          example: 0

    UpdateNodeTemplatesInput:
      type: object
      description: >
        Request body for updating a node's layout templates. The nodeId
        must match the URL path parameter.
      required:
        - nodeId
        - templates
      properties:
        nodeId:
          type: string
          format: uuid
          description: Node ID (must match the URL path parameter)
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        templates:
          type: array
          items:
            $ref: "#/components/schemas/EntityLayoutTemplate"
          description: Array of template definitions to set on the node

    # -------------------------------------------------------------------------
    # Schemas — Rules
    # -------------------------------------------------------------------------
    RuleEntity:
      type: object
      description: >
        A full rule definition including its JSON Schema body and
        metadata. Returned by GET and create/update operations.
      required:
        - componentKey
        - schema
        - deprecated
        - createdAt
        - etag
      properties:
        componentKey:
          type: string
          description: Unique key identifying this rule within its folder
          example: "SuperText"
        schema:
          type: object
          additionalProperties: true
          description: JSON Schema object defining the validation rule
          example:
            $schema: "https://json-schema.org/draft/2020-12/schema"
            type: "object"
            properties:
              text:
                type: "string"
                minLength: 1
        deprecated:
          type: boolean
          description: Whether this rule has been deprecated
          example: false
        createdAt:
          type: string
          format: date-time
          description: When this rule was created
          readOnly: true
          example: "2026-01-05T10:00:00Z"
        etag:
          type: string
          description: Entity tag for optimistic concurrency control
          readOnly: true
          example: "a1b2c3d4"

    RuleSummary:
      type: object
      description: >
        Lightweight rule metadata returned in folder listing. Does not
        include the full schema body.
      required:
        - folder
        - componentKey
        - deprecated
      properties:
        folder:
          type: string
          description: Folder this rule belongs to
          example: "layout"
        componentKey:
          type: string
          description: Unique key identifying this rule
          example: "SuperText"
        deprecated:
          type: boolean
          description: Whether this rule has been deprecated
          example: false
        createdAt:
          type: string
          format: date-time
          description: When this rule was created
          example: "2026-01-05T10:00:00Z"
        createdBy:
          $ref: "#/components/schemas/UserSummary"
        updatedAt:
          type: string
          format: date-time
          description: When this rule was last updated
          example: "2026-02-10T14:30:00Z"
        updatedBy:
          $ref: "#/components/schemas/UserSummary"
        label:
          type: string
          description: Optional human-readable label
          example: "Super Text Component"

    CreateRuleBody:
      type: object
      description: >
        Request body for creating or updating a rule. The schemaJson
        field must be a valid JSON string.
      required:
        - schemaJson
      properties:
        schemaJson:
          type: string
          description: Valid JSON string representing the rule's JSON Schema
          example: '{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"text":{"type":"string"}}}'

    # -------------------------------------------------------------------------
    # Schemas — Suggestions
    # -------------------------------------------------------------------------
    Suggestion:
      type: object
      description: >
        A user-submitted content proposal targeting a specific topic
        within an entity. Part of the Write path — suggestions compete
        via community voting to become consensus.
      required:
        - suggestionId
        - authorId
        - entityId
        - nodeId
        - topicKey
        - ruleKey
        - operation
        - value
        - status
        - score
        - upvotes
        - downvotes
        - createdAt
        - updatedAt
        - createdBy
        - updatedBy
      properties:
        suggestionId:
          type: string
          format: uuid
          description: Unique identifier, server-generated
          readOnly: true
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        authorId:
          type: string
          format: uuid
          description: User ID of the suggestion author
          readOnly: true
          example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
        entityId:
          type: string
          format: uuid
          description: Entity this suggestion targets
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        nodeId:
          type: string
          format: uuid
          description: Node the target entity belongs to
          readOnly: true
          example: "c3d4e5f6-a7b8-9012-cdef-123456789012"
        topicKey:
          type: string
          description: Topic slot this suggestion fills
          example: "description"
        itemKey:
          type:
            - string
            - "null"
          description: Item key for collection-type topics, null for single topics
          example: null
        ruleKey:
          type: string
          description: JSON Schema rule key used to validate this suggestion
          readOnly: true
          example: "SuperText"
        operation:
          type: string
          enum:
            - SET
            - ADD
            - UPDATE
            - DELETE
          description: The type of content operation this suggestion proposes
          example: "SET"
        value:
          description: >
            The proposed content value. Shape is validated against the
            topic's JSON Schema rule.
          example:
            text: "A bard who collects lore and stories from across the realms."
        status:
          type: string
          enum:
            - active
            - rejected
            - merged
            - spam
            - deleted
          description: Current lifecycle status of the suggestion
          example: "active"
        score:
          type: integer
          description: Net vote score (upvotes minus downvotes)
          readOnly: true
          example: 7
        upvotes:
          type: integer
          description: Total number of upvotes
          readOnly: true
          example: 9
        downvotes:
          type: integer
          description: Total number of downvotes
          readOnly: true
          example: 2
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of creation
          readOnly: true
          example: "2026-02-01T12:00:00Z"
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of last modification
          readOnly: true
          example: "2026-02-10T14:30:00Z"
        createdBy:
          $ref: "#/components/schemas/UserSummary"
        updatedBy:
          $ref: "#/components/schemas/UserSummary"
        deletedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of soft-deletion, if applicable
          readOnly: true
          example: null
        deletedBy:
          $ref: "#/components/schemas/UserSummary"

    SuggestionPage:
      type: object
      description: Paginated list of suggestions.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Suggestion"
          description: Suggestions in this page
        cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for fetching the next page, null if last page
          example: "eyJsYXN0S2V5Ijp7InBrIjoic3VnIn19"

    CreateSuggestionInput:
      type: object
      description: >
        Request body for creating a new suggestion. The value payload
        is validated against the topic's JSON Schema rule.
      required:
        - entityId
        - topicKey
        - operation
        - value
      properties:
        entityId:
          type: string
          minLength: 1
          description: Entity to submit the suggestion for
          example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
        topicKey:
          type: string
          minLength: 1
          description: Topic key this suggestion targets
          example: "description"
        itemKey:
          type:
            - string
            - "null"
          minLength: 1
          description: Optional item key for collection-type topics
          example: null
        operation:
          type: string
          enum:
            - SET
            - ADD
            - UPDATE
            - DELETE
          description: The type of content operation
          example: "SET"
        value:
          description: The proposed content value
          example:
            text: "A bard who collects lore and stories from across the realms."

    UpdateSuggestionInput:
      type: object
      description: >
        Request body for updating a suggestion. Both fields are optional
        but at least one must be provided. The suggestionId is taken
        from the URL path.
      properties:
        value:
          description: Updated content value
          example:
            text: "A bard who weaves magic through music and storytelling."
        status:
          type: string
          enum:
            - active
            - rejected
            - merged
            - spam
            - deleted
          description: Updated suggestion status
          example: "active"

    # -------------------------------------------------------------------------
    # Schemas — Users
    # -------------------------------------------------------------------------
    UserProfile:
      type: object
      description: >
        Public user profile. Email is always omitted from API responses
        to protect user privacy.
      required:
        - userId
        - username
        - rank
        - isActive
        - createdAt
        - updatedAt
      properties:
        userId:
          type: string
          format: uuid
          description: Unique identifier, server-generated
          readOnly: true
          example: "d4e5f6a7-b8c9-0123-def0-123456789abc"
        username:
          type: string
          description: Display username
          example: "dungeon_master_42"
        photoUrl:
          type:
            - string
            - "null"
          description: URL to the user's avatar image
          example: "https://cdn.example.com/avatars/dm42.webp"
        rank:
          type: string
          enum:
            - BEGINNER
            - NOVICE
            - ADEPT
            - EXPERT
            - MASTER
          description: User rank based on contribution level
          example: "ADEPT"
        bio:
          type:
            - string
            - "null"
          description: User's bio or description
          example: "Forever DM. Collector of mimics and bad puns."
        isActive:
          type: boolean
          description: Whether the user account is active
          example: true
        createdAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of account creation
          readOnly: true
          example: "2025-06-01T08:00:00Z"
        updatedAt:
          type: string
          format: date-time
          description: ISO 8601 timestamp of last profile update
          readOnly: true
          example: "2026-02-10T14:30:00Z"

    UserPage:
      type: object
      description: Paginated list of user profiles.
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/UserProfile"
          description: User profiles in this page
        cursor:
          type:
            - string
            - "null"
          description: Opaque cursor for fetching the next page, null if last page
          example: "eyJsYXN0S2V5Ijp7InBrIjoidXNyIn19"

    # -------------------------------------------------------------------------
    # Schemas — Votes
    # -------------------------------------------------------------------------
    CastVoteInput:
      type: object
      description: >
        Request body for casting a vote on a suggestion. Value 1 is
        upvote, -1 is downvote, 0 removes a previous vote.
      required:
        - suggestionId
        - value
      properties:
        suggestionId:
          type: string
          minLength: 1
          description: ID of the suggestion to vote on
          example: "b2c3d4e5-f6a7-8901-bcde-f12345678901"
        value:
          type: integer
          enum:
            - 1
            - 0
            - -1
          description: "Vote value: 1 (upvote), 0 (remove), -1 (downvote)"
          example: 1

    VoteDelta:
      type: object
      description: >
        Updated vote tallies returned after casting a vote. Reflects
        the new totals for the target suggestion.
      required:
        - score
        - upvotes
        - downvotes
      properties:
        score:
          type: integer
          description: Net vote score (upvotes minus downvotes)
          example: 7
        upvotes:
          type: integer
          description: Total number of upvotes
          example: 9
        downvotes:
          type: integer
          description: Total number of downvotes
          example: 2

    VoteMap:
      type: object
      description: >
        Map of the authenticated user's votes. Keys are suggestion IDs,
        values are vote values (1, 0, or -1).
      required:
        - votes
      properties:
        votes:
          type: object
          additionalProperties:
            type: integer
            enum:
              - 1
              - 0
              - -1
          description: Map of suggestionId to vote value
          example:
            b2c3d4e5-f6a7-8901-bcde-f12345678901: 1
            c3d4e5f6-a7b8-9012-cdef-234567890abc: -1

  # ---------------------------------------------------------------------------
  # Security Schemes
  # ---------------------------------------------------------------------------
  securitySchemes:
    cognitoAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >
        AWS Cognito JWT token. Obtain via Amplify Auth sign-in flow.
        Include as `Authorization: Bearer <token>` header.
