{
  "openapi": "3.1.0",
  "info": {
    "title": "RoadGate API",
    "version": "1.0.0",
    "description": "API pública de RoadGate: roadmaps trimestrales, work items jerárquicos\n(Epic → Feature → User Story), capacidad del equipo y métricas.\n\n## Autenticación\nTodas las llamadas requieren `Authorization: Bearer <token>`, donde el token es:\n\n- **API key de integración** (`rg_live_…`): se emite en Ajustes → API keys y\n  se muestra una sola vez. Sus permisos vienen dados por sus *scopes*.\n- **Access token de sesión** (JWT): lo usa el propio frontend de RoadGate.\n\n## Scopes\n- `roadmaps:read` — lecturas (GET).\n- `roadmaps:write` — creación, actualización y borrado.\n\nLa gestión de API keys (`/api-keys`) sólo admite sesión de usuario: una API key\nno puede emitir ni revocar otras claves.\n\n## Errores\nTodo error devuelve `{ \"error\": { \"code\", \"message\", \"details?\" } }`.\nEl `code` es estable y es lo que debe inspeccionar un integrador.\n\n## CORS\nTodos los endpoints admiten `OPTIONS` (preflight) y responden con\n`Access-Control-Allow-Origin: *`, por lo que la API puede consumirse desde el navegador."
  },
  "servers": [
    {
      "url": "/api/public/v1",
      "description": "Versión 1 de la API"
    }
  ],
  "tags": [
    {
      "name": "Roadmaps",
      "description": "Alta, consulta, renombrado y borrado de roadmaps."
    },
    {
      "name": "Work items",
      "description": "Snapshot completo de los items de un roadmap."
    },
    {
      "name": "Capacity",
      "description": "Capacidad del equipo y su historial de cambios."
    },
    {
      "name": "Stats",
      "description": "Métricas agregadas del workspace."
    },
    {
      "name": "API keys",
      "description": "Credenciales de integración (sólo con sesión)."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/roadmaps": {
      "get": {
        "tags": [
          "Roadmaps"
        ],
        "summary": "Lista los roadmaps del actor",
        "operationId": "listRoadmaps",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Roadmaps ordenados por fecha de actualización.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/RoadmapSummary"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "Roadmaps"
        ],
        "summary": "Crea un roadmap vacío",
        "operationId": "createRoadmap",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 200
                  }
                }
              },
              "example": {
                "name": "Roadmap 2026"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Roadmap creado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/roadmaps/{roadmapId}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RoadmapId"
        }
      ],
      "get": {
        "tags": [
          "Roadmaps"
        ],
        "summary": "Carga completa de un roadmap",
        "description": "Devuelve la cabecera, todos sus work items y su configuración de capacidad.",
        "operationId": "getRoadmap",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Roadmap completo.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RoadmapDetail"
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "tags": [
          "Roadmaps"
        ],
        "summary": "Renombra un roadmap",
        "operationId": "renameRoadmap",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Roadmaps"
        ],
        "summary": "Borra un roadmap",
        "description": "Elimina en cascada sus work items, su capacidad y su historial.",
        "operationId": "deleteRoadmap",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/items": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RoadmapId"
        }
      ],
      "put": {
        "tags": [
          "Work items"
        ],
        "summary": "Reemplaza todos los work items",
        "description": "Estrategia *replace-all*: el cuerpo es el snapshot completo del roadmap. Los items que no aparezcan se eliminan.",
        "operationId": "replaceItems",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "items"
                ],
                "properties": {
                  "items": {
                    "type": "array",
                    "maxItems": 10000,
                    "items": {
                      "$ref": "#/components/schemas/RoadmapItem"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Snapshot guardado.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "const": true
                    },
                    "count": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/capacity": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RoadmapId"
        }
      ],
      "put": {
        "tags": [
          "Capacity"
        ],
        "summary": "Guarda la capacidad del roadmap",
        "description": "Registra además el audit trail de los campos modificados (consultable en `/roadmaps/{roadmapId}/capacity/history`).",
        "operationId": "saveCapacity",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "capacity"
                ],
                "properties": {
                  "capacity": {
                    "$ref": "#/components/schemas/CapacityConfig"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/capacity/history": {
      "parameters": [
        {
          "$ref": "#/components/parameters/RoadmapId"
        }
      ],
      "get": {
        "tags": [
          "Capacity"
        ],
        "summary": "Historial de cambios de capacidad",
        "operationId": "getCapacityHistory",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Cambios ordenados del más reciente al más antiguo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CapacityHistoryEntry"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/billing/subscription": {
      "get": {
        "tags": [
          "Billing"
        ],
        "summary": "Estado comercial del equipo",
        "description": "Plan, estado de suscripción (trialing/active/past_due/grace_period/cancelled), asientos usados y límites. La fuente de verdad es el backend.",
        "operationId": "getBillingState",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Estado comercial.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "plan": {
                      "type": "string",
                      "enum": [
                        "solo",
                        "team",
                        "business"
                      ]
                    },
                    "status": {
                      "type": "string"
                    },
                    "effectiveStatus": {
                      "type": "string"
                    },
                    "readOnly": {
                      "type": "boolean"
                    },
                    "trialEndsAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "graceEndsAt": {
                      "type": "string",
                      "nullable": true
                    },
                    "seatLimit": {
                      "type": "integer"
                    },
                    "seatsUsed": {
                      "type": "integer"
                    },
                    "overSeatLimit": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/billing/checkout": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Abrir checkout de suscripción (sólo Team Admin)",
        "description": "Devuelve 403 a cualquier miembro que no sea Team Admin y 501 mientras no haya proveedor de pago aprobado.",
        "operationId": "startCheckout",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:write"
            ]
          }
        ],
        "responses": {
          "403": {
            "description": "El actor no es Team Admin."
          },
          "501": {
            "description": "Proveedor de pago no configurado."
          }
        }
      }
    },
    "/billing/webhook": {
      "post": {
        "tags": [
          "Billing"
        ],
        "summary": "Webhook de suscripciones (proveedor de pago)",
        "description": "Única vía de cambio de plan/estado/asientos. Exige firma HMAC-SHA256 en `X-Signature` y es idempotente por `eventId`.",
        "operationId": "billingWebhook",
        "responses": {
          "200": {
            "description": "Evento aplicado o ignorado por duplicado."
          },
          "401": {
            "description": "Firma inválida."
          },
          "503": {
            "description": "Proveedor no configurado."
          }
        }
      }
    },
    "/teams/me": {
      "get": {
        "tags": [
          "Teams"
        ],
        "summary": "Equipo activo del usuario",
        "description": "Devuelve la cuenta de equipo del actor; se provisiona automáticamente la primera vez.",
        "operationId": "getActiveTeam",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "responses": {
          "200": {
            "description": "Equipo activo.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    },
                    "plan": {
                      "type": "string"
                    },
                    "seatLimit": {
                      "type": "integer"
                    },
                    "role": {
                      "type": "string",
                      "enum": [
                        "admin",
                        "member"
                      ]
                    },
                    "memberId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/members": {
      "get": {
        "summary": "Quién tiene acceso al roadmap (o candidatos con ?candidates=1)",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "candidates",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Lista de accesos"
          },
          "403": {
            "description": "Sin permiso"
          }
        }
      },
      "post": {
        "summary": "Compartir el roadmap con un miembro activo del equipo (sólo Admin)",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "teamMemberId",
                  "role"
                ],
                "properties": {
                  "teamMemberId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "role": {
                    "type": "string",
                    "enum": [
                      "editor",
                      "viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Acceso concedido"
          },
          "403": {
            "description": "Sólo el Roadmap Admin"
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/members/{memberId}": {
      "patch": {
        "summary": "Cambiar el permiso (Editor ↔ Viewer). Sólo Admin",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "role"
                ],
                "properties": {
                  "role": {
                    "type": "string",
                    "enum": [
                      "editor",
                      "viewer"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Permiso actualizado"
          }
        }
      },
      "delete": {
        "summary": "Retirar el acceso de inmediato. Sólo Admin",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Acceso retirado"
          }
        }
      }
    },
    "/roadmaps/{roadmapId}/transfer": {
      "post": {
        "summary": "Transferir la administración del roadmap (el anterior Admin pasa a Editor)",
        "tags": [
          "Sharing"
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "teamMemberId"
                ],
                "properties": {
                  "teamMemberId": {
                    "type": "string",
                    "format": "uuid"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Administración transferida"
          }
        }
      }
    },
    "/teams/members": {
      "get": {
        "tags": [
          "Teams"
        ],
        "summary": "Miembros del equipo",
        "description": "Lista los miembros del equipo activo (activos e inactivos).",
        "operationId": "listTeamMembers",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Miembros del equipo."
          }
        }
      }
    },
    "/teams/audit-events": {
      "get": {
        "tags": [
          "Teams"
        ],
        "summary": "Actividad administrativa",
        "description": "Sólo Team Admin. Eventos de invitación, cambio de estado de miembros y cambios de permisos o administración de roadmaps, acotados al equipo.",
        "operationId": "listAuditEvents",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Eventos administrativos."
          },
          "403": {
            "description": "No es admin."
          }
        }
      }
    },
    "/teams/members/{memberId}": {
      "get": {
        "tags": [
          "Teams"
        ],
        "summary": "Roadmaps que administra el miembro",
        "description": "Sólo Team Admin. Lista los roadmaps cuya administración hay que transferir antes de desactivar a esa persona.",
        "operationId": "listMemberAdminRoadmaps",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Roadmaps administrados."
          },
          "403": {
            "description": "No es admin."
          }
        }
      },
      "patch": {
        "tags": [
          "Teams"
        ],
        "summary": "Activar o desactivar un miembro",
        "description": "Sólo Team Admin. Desactivar retira el acceso de inmediato, sin borrar datos.",
        "operationId": "setTeamMemberStatus",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "memberId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Estado actualizado."
          },
          "403": {
            "description": "No es admin."
          }
        }
      }
    },
    "/teams/invitations": {
      "get": {
        "tags": [
          "Teams"
        ],
        "summary": "Invitaciones del equipo",
        "description": "Sólo Team Admin.",
        "operationId": "listTeamInvitations",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Invitaciones."
          }
        }
      },
      "post": {
        "tags": [
          "Teams"
        ],
        "summary": "Invitar por email",
        "description": "Sólo Team Admin. El invitado entra como Team Member. La invitación caduca en 7 días.",
        "operationId": "inviteTeamMember",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "201": {
            "description": "Invitación creada."
          },
          "409": {
            "description": "Email ya invitado o ya miembro."
          }
        }
      }
    },
    "/teams/invitations/{invitationId}": {
      "post": {
        "tags": [
          "Teams"
        ],
        "summary": "Reenviar invitación",
        "description": "Sólo Team Admin. Rota el token y renueva la caducidad.",
        "operationId": "resendTeamInvitation",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "invitationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invitación reenviada."
          }
        }
      },
      "delete": {
        "tags": [
          "Teams"
        ],
        "summary": "Revocar invitación",
        "operationId": "revokeTeamInvitation",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "invitationId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invitación revocada."
          }
        }
      }
    },
    "/teams/invitations/accept": {
      "post": {
        "tags": [
          "Teams"
        ],
        "summary": "Aceptar invitación",
        "description": "Requiere sesión. Valida token, caducidad, revocación y email. Es idempotente.",
        "operationId": "acceptTeamInvitation",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Invitación aceptada."
          },
          "403": {
            "description": "Token caducado, revocado o de otro email."
          }
        }
      }
    },
    "/stats": {
      "get": {
        "tags": [
          "Stats"
        ],
        "summary": "Métricas del workspace",
        "description": "Sin `roadmapId` agrega todo el workspace; con él, sólo ese roadmap.",
        "operationId": "getWorkspaceStats",
        "security": [
          {
            "bearerAuth": [
              "roadmaps:read"
            ]
          }
        ],
        "parameters": [
          {
            "name": "roadmapId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Filtra las métricas a un roadmap concreto."
          }
        ],
        "responses": {
          "200": {
            "description": "Métricas agregadas.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorkspaceStats"
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys": {
      "get": {
        "tags": [
          "API keys"
        ],
        "summary": "Lista las API keys del usuario",
        "description": "Requiere sesión de usuario; no se puede llamar con una API key.",
        "operationId": "listApiKeys",
        "responses": {
          "200": {
            "description": "Claves del usuario, sin secretos.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ApiKey"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "API keys"
        ],
        "summary": "Emite una API key",
        "description": "El secreto (`key`) se devuelve **una única vez**: en la base de datos sólo se guarda su hash SHA-256.",
        "operationId": "createApiKey",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "name"
                ],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 120
                  },
                  "scopes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "roadmaps:read",
                        "roadmaps:write"
                      ]
                    }
                  },
                  "expiresInDays": {
                    "type": "integer",
                    "minimum": 1
                  }
                }
              },
              "example": {
                "name": "Integración Jira",
                "scopes": [
                  "roadmaps:read",
                  "roadmaps:write"
                ],
                "expiresInDays": 365
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Clave emitida.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "key": {
                      "type": "string",
                      "example": "rg_live_…"
                    },
                    "apiKey": {
                      "$ref": "#/components/schemas/ApiKey"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api-keys/{keyId}": {
      "parameters": [
        {
          "name": "keyId",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          },
          "description": "Identificador de la API key."
        }
      ],
      "delete": {
        "tags": [
          "API keys"
        ],
        "summary": "Revoca o borra una API key",
        "description": "Por defecto revoca (marca `revokedAt` y conserva la traza). Con `?purge=true` elimina la fila por completo.",
        "operationId": "revokeApiKey",
        "parameters": [
          {
            "name": "purge",
            "in": "query",
            "required": false,
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "`true` borra la fila en lugar de revocarla."
          }
        ],
        "responses": {
          "200": {
            "$ref": "#/components/responses/Ok"
          },
          "400": {
            "description": "Entrada inválida (`validation_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Credenciales ausentes o inválidas (`unauthorized`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Falta el scope requerido (`forbidden`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "El recurso no existe o no pertenece al actor (`not_found`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Error interno (`internal_error`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key de integración (`rg_live_…`) o access token JWT de la sesión de RoadGate."
      }
    },
    "parameters": {
      "RoadmapId": {
        "name": "roadmapId",
        "in": "path",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Identificador del roadmap."
      }
    },
    "responses": {
      "Ok": {
        "description": "Operación completada.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "ok": {
                  "const": true
                }
              }
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "validation_error",
                  "unauthorized",
                  "forbidden",
                  "not_found",
                  "conflict",
                  "internal_error"
                ],
                "description": "Código estable, apto para lógica de cliente."
              },
              "message": {
                "type": "string",
                "description": "Mensaje legible para humanos."
              },
              "details": {
                "description": "Detalle de validación cuando aplica."
              }
            }
          }
        },
        "example": {
          "error": {
            "code": "not_found",
            "message": "Roadmap not found"
          }
        }
      },
      "RoadmapSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "createdAt",
          "updatedAt",
          "itemCount"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          },
          "itemCount": {
            "type": "integer",
            "description": "Número de work items del roadmap."
          }
        }
      },
      "RoadmapDetail": {
        "type": "object",
        "required": [
          "roadmap",
          "items",
          "capacity"
        ],
        "properties": {
          "roadmap": {
            "type": "object",
            "required": [
              "id",
              "name"
            ],
            "properties": {
              "id": {
                "type": "string",
                "format": "uuid"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/RoadmapItem"
            }
          },
          "capacity": {
            "$ref": "#/components/schemas/CapacityConfig"
          }
        }
      },
      "RoadmapItem": {
        "type": "object",
        "required": [
          "uid",
          "id",
          "type"
        ],
        "description": "Work item de la jerarquía Epic → Feature → User Story. `uid` es la clave estable interna; `id` es el código visible y es lo que referencia `parentId`.",
        "properties": {
          "uid": {
            "type": "string",
            "minLength": 1,
            "description": "Clave estable interna."
          },
          "id": {
            "type": "string",
            "maxLength": 120,
            "description": "Código visible (p. ej. EPIC-01)."
          },
          "type": {
            "type": "string",
            "enum": [
              "epic",
              "feature",
              "story"
            ]
          },
          "title": {
            "type": "string",
            "maxLength": 500,
            "default": ""
          },
          "description": {
            "type": "string",
            "maxLength": 20000
          },
          "parentId": {
            "type": "string",
            "maxLength": 120,
            "description": "`id` del item padre."
          },
          "effort": {
            "type": "number",
            "minimum": 0,
            "description": "Horas. En los padres es la suma de sus hijos."
          },
          "priority": {
            "type": "string",
            "enum": [
              "1-High",
              "2-Medium",
              "3-Low",
              "4-Lowest",
              ""
            ]
          },
          "quarter": {
            "type": "string",
            "enum": [
              "Q1",
              "Q2",
              "Q3",
              "Q4",
              "MULTI",
              ""
            ],
            "description": "Vacío = backlog. `MULTI` = hijos repartidos entre varios quarters."
          },
          "sprint": {
            "type": "integer",
            "minimum": 1,
            "deprecated": true
          },
          "state": {
            "type": "string",
            "enum": [
              "Backlog",
              "In Progress",
              "Done",
              "Blocked"
            ]
          },
          "notes": {
            "type": "string",
            "maxLength": 20000
          },
          "tags": {
            "type": "string",
            "maxLength": 2000
          },
          "displayMode": {
            "type": "string",
            "enum": [
              "auto",
              "self",
              "children"
            ]
          },
          "hiddenFromRoadmap": {
            "type": "boolean",
            "default": false
          }
        }
      },
      "CapacityConfig": {
        "type": "object",
        "required": [
          "developers",
          "dedicationPct",
          "daysPerSprint",
          "hoursPerDay",
          "sprintsPerQuarter"
        ],
        "description": "Capacidad = devs × dedicación × sprints × días × horas. `hoursByQuarter` tiene prioridad sobre ese cálculo.",
        "properties": {
          "developers": {
            "type": "integer",
            "minimum": 0,
            "maximum": 10000
          },
          "dedicationPct": {
            "type": "number",
            "minimum": 0,
            "maximum": 100
          },
          "daysPerSprint": {
            "type": "integer",
            "minimum": 0,
            "maximum": 365
          },
          "hoursPerDay": {
            "type": "number",
            "minimum": 0,
            "maximum": 24
          },
          "sprintsPerQuarter": {
            "type": "integer",
            "minimum": 0,
            "maximum": 52
          },
          "sprintsByQuarter": {
            "$ref": "#/components/schemas/ByQuarterNumber"
          },
          "hoursByQuarter": {
            "$ref": "#/components/schemas/ByQuarterNumber"
          }
        }
      },
      "ByQuarterNumber": {
        "type": "object",
        "description": "Overrides por quarter real.",
        "properties": {
          "Q1": {
            "type": "number",
            "minimum": 0
          },
          "Q2": {
            "type": "number",
            "minimum": 0
          },
          "Q3": {
            "type": "number",
            "minimum": 0
          },
          "Q4": {
            "type": "number",
            "minimum": 0
          }
        },
        "additionalProperties": false
      },
      "CapacityHistoryEntry": {
        "type": "object",
        "required": [
          "id",
          "field",
          "by",
          "at"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "field": {
            "type": "string",
            "description": "Campo modificado (p. ej. `hoursByQuarter.Q3`)."
          },
          "oldValue": {
            "type": "string",
            "nullable": true
          },
          "newValue": {
            "type": "string",
            "nullable": true
          },
          "by": {
            "type": "string",
            "description": "Email de quien hizo el cambio."
          },
          "at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "WorkspaceStats": {
        "type": "object",
        "required": [
          "roadmapsCount",
          "teamsCount",
          "totalDevelopers",
          "totalItems",
          "byType"
        ],
        "properties": {
          "roadmapsCount": {
            "type": "integer"
          },
          "teamsCount": {
            "type": "integer"
          },
          "totalDevelopers": {
            "type": "integer"
          },
          "totalItems": {
            "type": "integer"
          },
          "byType": {
            "type": "object",
            "properties": {
              "epic": {
                "type": "integer"
              },
              "feature": {
                "type": "integer"
              },
              "story": {
                "type": "integer"
              }
            }
          }
        }
      },
      "ApiKey": {
        "type": "object",
        "required": [
          "id",
          "name",
          "prefix",
          "scopes",
          "createdAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "prefix": {
            "type": "string",
            "description": "Prefijo visible para identificar la clave."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "roadmaps:read",
                "roadmaps:write"
              ]
            }
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "lastUsedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "revokedAt": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      }
    }
  }
}