# Generated by scripts/gen_openapi.mjs from the registered route table.
# Do not edit by hand: regenerate it.
openapi: 3.1.0
info:
  title: Verify local API
  version: 2.0.0-candidate
  summary: The API of one local Verify installation.
  description: |-
      This document is generated from the route table this build registers, and the
      generator fails if an operation in the contract is missing from that table or
      an unexpected one appears. It describes a loopback installation only.
      
      An HTTP 200 reports transport. The outcome of an assessment is in
      comparison_status, record_valid and security_assessment_status, and
      release_authority is false in every response of every mode.
      
      Not deployed. No DNS, no TLS, no public origin is created by this product.
servers:
  - url: http://127.0.0.1:4177
    description: "This installation, loopback only"
components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: verify_session
      description: Browser session. Mutations additionally require the x-verify-csrf header and an exact Origin.
    bearerToken:
      type: http
      scheme: bearer
      description: "Scoped token, bound to one space and optionally one project. Never accepted together with a session cookie."
  schemas:
    AssessmentRequest:
      title: Verify R2 AssessmentRequest
      type: object
      additionalProperties: false
      required:
        - mode
        - artifact_id
        - requirement_version_id
      properties:
        mode:
          enum:
            - identity
            - bundle
        artifact_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
        reference_version_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
        requirement_version_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
        bundle_evidence_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
        pins_evidence_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
      allOf:
        - if:
            properties:
              mode:
                const: identity
          then:
            required:
              - reference_version_id
        - if:
            properties:
              mode:
                const: bundle
          then:
            required:
              - bundle_evidence_id
              - pins_evidence_id
    AssessmentResult:
      title: "Verify R2 result envelope, not an AC7 schema replacement"
      type: object
      additionalProperties: false
      required:
        - assessment_id
        - mode
        - job_status
        - execution_mode
        - kernel_invoked
        - comparison_status
        - record_valid
        - security_assessment_status
        - release_authority
        - completeness
        - provenance_status
        - limits
      properties:
        assessment_id:
          type: string
          minLength: 1
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
        mode:
          enum:
            - identity
            - bundle
        job_status:
          enum:
            - DRAFT
            - VALIDATING
            - NEEDS_INPUT
            - QUEUED
            - RUNNING
            - COMPLETED
            - ERROR
            - INTERRUPTED
            - CANCELLED
        execution_mode:
          enum:
            - IDENTITY_CHECK
            - LIVE_REFERENCE_KERNEL
            - RECORDED_REPLAY
            - NONE
        kernel_invoked:
          type: boolean
        comparison_status:
          enum:
            - MATCH
            - MISMATCH
            - NO_REFERENCE
            - NOT_RUN
        record_valid:
          type:
            - boolean
            - "null"
        security_assessment_status:
          enum:
            - PASS
            - FAIL
            - NO_CONSTA
            - null
        release_authority:
          const: false
        completeness:
          enum:
            - COMPLETE
            - INCOMPLETE
            - NOT_APPLICABLE
        provenance_status:
          enum:
            - LOCAL_MEASUREMENT
            - USER_DECLARED
            - ORG_APPROVED_REFERENCE
            - IMPORTED_ASSERTION
            - REFERENCE_FIXTURE
            - MIXED_SOURCES
        artifact_sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
        expected_sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
        input_snapshot_digest:
          type: string
          pattern: "^[0-9a-f]{64}$"
        kernel_content_root_sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
        kernel_archive_sha256:
          type: string
          pattern: "^[0-9a-f]{64}$"
        raw_kernel_result:
          type:
            - object
            - "null"
        limits:
          type: array
          items:
            type: string
        causes:
          type: array
          items:
            type: object
        evaluated_at:
          type:
            - string
            - "null"
        report_id:
          type:
            - string
            - "null"
      allOf:
        - if:
            properties:
              mode:
                const: identity
          then:
            properties:
              kernel_invoked:
                const: false
              record_valid:
                type: "null"
              security_assessment_status:
                type: "null"
        - if:
            properties:
              security_assessment_status:
                enum:
                  - PASS
                  - FAIL
                  - NO_CONSTA
          then:
            properties:
              mode:
                const: bundle
              kernel_invoked:
                const: true
              execution_mode:
                const: LIVE_REFERENCE_KERNEL
        - if:
            properties:
              security_assessment_status:
                const: PASS
            required:
              - security_assessment_status
          then:
            properties:
              record_valid:
                const: true
              completeness:
                const: COMPLETE
              job_status:
                const: COMPLETED
        - if:
            properties:
              job_status:
                enum:
                  - DRAFT
                  - VALIDATING
                  - NEEDS_INPUT
                  - QUEUED
                  - RUNNING
                  - ERROR
                  - INTERRUPTED
                  - CANCELLED
            required:
              - job_status
          then:
            properties:
              security_assessment_status:
                type: "null"
              comparison_status:
                enum:
                  - NOT_RUN
                  - NO_REFERENCE
        - if:
            properties:
              comparison_status:
                enum:
                  - MATCH
                  - MISMATCH
            required:
              - comparison_status
          then:
            required:
              - artifact_sha256
              - expected_sha256
            properties:
              job_status:
                const: COMPLETED
        - if:
            properties:
              mode:
                const: identity
              job_status:
                const: COMPLETED
            required:
              - mode
              - job_status
          then:
            properties:
              execution_mode:
                const: IDENTITY_CHECK
              comparison_status:
                enum:
                  - MATCH
                  - MISMATCH
      $comment: "Schema checks representation only; equality of digests, correspondence to raw AC7, scope completeness and real execution must be rederived by implementation and tested."
    Health:
      type: object
      description: "Liveness: el proceso responde. NO dice si puede aceptar trabajo y NO es un veredicto. Público y sin autenticar, así que no lleva versión, host, recuento, llave, hash interno ni ruta privada."
      properties:
        status:
          type: string
          enum:
            - ok
          description: "Único valor: el proceso contesta"
        scope:
          type: string
          enum:
            - HTTP_LIVENESS_ONLY
          description: "El límite va en el propio cuerpo, no en la documentación"
        release_authority:
          type: boolean
          const: false
          description: "Siempre false. Un 200 aquí no autoriza nada"
      required:
        - status
        - scope
        - release_authority
      additionalProperties: false
    Readiness:
      type: object
      description: "Disponibilidad mínima para aceptar trabajo. `healthz` (proceso vivo) es otra pregunta distinta, y el detalle para el operador es una tercera. Público y de grano grueso: la lista de bloqueo son CATEGORÍAS."
      properties:
        ready:
          type: boolean
          description: Si acepta trabajos nuevos
        blocking:
          type: array
          items:
            type: string
            enum:
              - schema
              - database
              - storage
              - evaluator_profile
              - retention_policy
            description: Dependencia que impide aceptar trabajo
          description: "Vacío cuando ready=true. Nunca incluye llaves, hashes internos, versiones, hosts, recuentos ni rutas privadas"
          maxItems: 10
        scope:
          type: string
          enum:
            - SERVICE_READINESS_ONLY
          description: "El límite va en el propio cuerpo"
        release_authority:
          type: boolean
          const: false
          description: "Siempre false. ready=true no autoriza nada"
        checked_at:
          type: string
          format: date-time
          description: "Momento de la comprobación (RFC 3339, UTC, reloj del servidor: no es un reloj externo de confianza)"
      required:
        - ready
        - blocking
        - scope
        - release_authority
        - checked_at
      additionalProperties: false
    OperatorReadiness:
      type: object
      description: "Detalle de readiness para el OPERADOR. Exige administrador de instancia, y por eso aquí sí van la versión del producto y el perfil activo. el 503 devuelve el MISMO cuerpo que el 200 con ready=false: este endpoint existe para transportar por qué no se acepta trabajo, y un cuerpo de error genérico borraría justo esa información"
      properties:
        ready:
          type: boolean
          description: Si acepta trabajos nuevos
        product_version:
          type: string
          minLength: 1
          maxLength: 40
          description: "Versión del producto"
        profile:
          type: string
          minLength: 1
          maxLength: 20
          description: "Perfil de instalación activo"
        checks:
          type: array
          items:
            type: object
            description: "Comprobación de una dependencia"
            properties:
              name:
                type: string
                minLength: 1
                maxLength: 60
                description: Dependencia
              ok:
                type: boolean
              detail:
                type: string
                minLength: 1
                maxLength: 300
                description: "Causa, sin secretos ni rutas privadas"
            required:
              - name
              - ok
              - detail
            additionalProperties: false
          description: Una por dependencia. En el 503 es donde vive el motivo
          maxItems: 20
        release_authority:
          type: boolean
          const: false
          description: Siempre false
        checked_at:
          type: string
          format: date-time
          description: "Momento de la comprobación (RFC 3339, UTC, reloj del servidor: no es un reloj externo de confianza)"
      required:
        - ready
        - product_version
        - profile
        - checks
        - release_authority
        - checked_at
      additionalProperties: false
    SiteModeState:
      type: object
      description: "Modo portada (CONTRATO-PORTADA-1.1.md §2.3). El mismo cuerpo responde siteMode.get y siteMode.set."
      additionalProperties: false
      required:
        - mode
        - changed_at
        - changed_by
      properties:
        mode:
          type: string
          enum:
            - OPEN
            - PORTADA
          description: "El estado vigente de la instalación."
        changed_at:
          type:
            - string
            - "null"
          format: date-time
          description: "Del último evento site_mode.change. null si nunca se cambió — nunca se rellena con la fecha de instalación."
        changed_by:
          type:
            - string
            - "null"
          pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: "El id del usuario que hizo el último cambio efectivo. null si nunca se cambió."
    SiteModeSetRequest:
      type: object
      description: "Modo portada (CONTRATO-PORTADA-1.1.md §2.3). El cuerpo de PUT /api/v1/admin/site-mode."
      additionalProperties: false
      required:
        - mode
      properties:
        mode:
          type: string
          enum:
            - OPEN
            - PORTADA
          description: "El estado al que se quiere mover la instalación."
    PageInfo:
      type: object
      description: "apps/server/src/http.ts's PageInfoOut, exactly — the shape every paged listing's `page` field carries. H-005 (Codex 2026-10-10): referenced once, from every success_kind: page operation's response_schema, rather than retyped 15 times."
      additionalProperties: false
      required:
        - next_cursor
        - has_more
        - limit
      properties:
        next_cursor:
          type:
            - string
            - "null"
          description: Opaque. null means the listing is exhausted.
        has_more:
          type: boolean
        limit:
          type: integer
          minimum: 1
  responses:
    Error:
      description: Uniform error shape. No stack trace and no server path.
      content:
        application/json:
          schema:
            type: object
            required:
              - error
            properties:
              error:
                type: object
                required:
                  - code
                  - message_key
                  - request_id
                  - details
                properties:
                  code:
                    type: string
                  message_key:
                    type: string
                  request_id:
                    type: string
                  details:
                    type: array
                    items: {}
paths:
  "/api/v1/spaces/{spaceId}/artifacts/{artifactId}/complete":
    post:
      operationId: artifacts.complete
      summary: Artefacto publicado y visible
      tags:
        - artifacts
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: artifactId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - upload_id
              properties:
                upload_id:
                  type: string
                  minLength: 1
                  maxLength: 64
                observed_sha256:
                  type:
                    - string
                    - "null"
                  pattern: "^[a-f0-9]{64}$"
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - project_id
                  - filename
                  - state
                  - blob
                  - declared_sha256
                  - created_at
                  - created_by
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  project_id:
                    type: string
                  filename:
                    type: string
                  state:
                    type: string
                    enum:
                      - PENDING
                      - READY
                      - ORPHAN
                      - PURGED
                  blob:
                    type:
                      - object
                      - "null"
                    additionalProperties: false
                    required:
                      - blob_id
                      - sha256
                      - size_bytes
                      - media_type
                      - class
                      - stored_at
                    properties:
                      blob_id:
                        type: string
                      sha256:
                        type: string
                        pattern: "^[a-f0-9]{64}$"
                      size_bytes:
                        type: integer
                        minimum: 0
                      media_type:
                        type: string
                      class:
                        type: string
                        enum:
                          - artifact
                          - evidence
                          - raw_result
                          - export
                          - import
                      stored_at:
                        type: string
                        format: date-time
                  declared_sha256:
                    type:
                      - string
                      - "null"
                    pattern: "^[a-f0-9]{64}$"
                  created_at:
                    type: string
                    format: date-time
                  created_by:
                    type: object
                    additionalProperties: false
                    required:
                      - kind
                      - user_id
                      - token_id
                      - display
                    properties:
                      kind:
                        type: string
                        enum:
                          - USER
                          - API_TOKEN
                          - SYSTEM
                          - INSTANCE_OWNER
                      user_id:
                        type:
                          - string
                          - "null"
                      token_id:
                        type:
                          - string
                          - "null"
                      display:
                        type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: UPLOAD_ALREADY_COMPLETED
              cause: "la reserva ya se consumió: un segundo complete es conflicto, no una segunda publicación · medido en RC01 routes/artifacts_staged.ts"
            - code: ARTIFACT_NOT_PENDING
              cause: "el artefacto ya no está PENDING: un publicado conserva su medición · medido en RC01 routes/artifacts_staged.ts"
            - code: IDEMPOTENCY_RESULT_UNBOUND
              cause: "la clave existe y su resultado no apunta a nada · medido en RC01 routes/artifacts_staged.ts"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
            - code: UPLOAD_NOT_COMPLETABLE
              cause: "la reserva no está en BYTES_RECEIVED: no hay bytes que publicar todavía · medido en routes/artifacts_staged.ts:327"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: PAYLOAD_TOO_LARGE
              cause: "los bytes transferidos exceden lo reservado · medido en routes/artifacts_staged.ts:211"
        422:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: UPLOAD_HAS_NO_BYTES
              cause: "se pide publicar una reserva a la que no han llegado bytes: no hay medición que contrastar · medido en RC01 routes/artifacts_staged.ts"
            - code: BLOB_OBJECT_MISSING
              cause: "la fila del blob existe y su objeto no está en el almacén · medido en RC01 routes/artifacts_staged.ts"
            - code: BLOB_SIZE_DRIFTED
              cause: "el tamaño medido al publicar no es el medido al recibir · medido en RC01 routes/artifacts_staged.ts"
            - code: DIGEST_MISMATCH
              cause: "el sha256 declarado al abrir la reserva no coincide con el medido en los bytes recibidos · medido en routes/artifacts_staged.ts:368"
      x-verify-permission: "OWNER u OPERATOR · artifacts.upload"
      x-verify-permission-capability: artifacts.upload
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: artifacts.upload
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/artifacts/{artifactId}/download":
    get:
      operationId: artifacts.download
      summary: Bytes del artefacto por proxy autenticado de la API
      tags:
        - artifacts
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: artifactId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: binary
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: BLOB_OBJECT_MISSING
              cause: "el objeto no está en el almacén · medido en RC01 routes/artifacts_staged.ts"
            - code: ARTIFACT_NOT_READY
              cause: "el artefacto referido no está READY: todavía no se completó su publicación · medido en routes/artifacts_staged.ts:448"
      x-verify-permission: "miembro · artifacts.read"
      x-verify-permission-capability: artifacts.read
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/artifacts/{artifactId}":
    get:
      operationId: artifacts.get
      summary: Artefacto
      tags:
        - artifacts
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: artifactId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - project_id
                  - filename
                  - state
                  - blob
                  - declared_sha256
                  - created_at
                  - created_by
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  project_id:
                    type: string
                  filename:
                    type: string
                  state:
                    type: string
                    enum:
                      - PENDING
                      - READY
                      - ORPHAN
                      - PURGED
                  blob:
                    type:
                      - object
                      - "null"
                    additionalProperties: false
                    required:
                      - blob_id
                      - sha256
                      - size_bytes
                      - media_type
                      - class
                      - stored_at
                    properties:
                      blob_id:
                        type: string
                      sha256:
                        type: string
                        pattern: "^[a-f0-9]{64}$"
                      size_bytes:
                        type: integer
                        minimum: 0
                      media_type:
                        type: string
                      class:
                        type: string
                        enum:
                          - artifact
                          - evidence
                          - raw_result
                          - export
                          - import
                      stored_at:
                        type: string
                        format: date-time
                  declared_sha256:
                    type:
                      - string
                      - "null"
                    pattern: "^[a-f0-9]{64}$"
                  created_at:
                    type: string
                    format: date-time
                  created_by:
                    type: object
                    additionalProperties: false
                    required:
                      - kind
                      - user_id
                      - token_id
                      - display
                    properties:
                      kind:
                        type: string
                        enum:
                          - USER
                          - API_TOKEN
                          - SYSTEM
                          - INSTANCE_OWNER
                      user_id:
                        type:
                          - string
                          - "null"
                      token_id:
                        type:
                          - string
                          - "null"
                      display:
                        type: string
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ARTIFACT_NOT_AVAILABLE
              cause: "artefacto no visible para esta identidad, incluido el de otro espacio · medido en routes/assessments.ts:42"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
  "/api/v1/spaces/{spaceId}/projects/{projectId}/artifacts/init":
    post:
      operationId: artifacts.initUpload
      summary: "Reserva creada; el artefacto aún NO es visible"
      tags:
        - artifacts
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - filename
                - media_type
                - purpose
                - size_bytes
              properties:
                filename:
                  type: string
                  minLength: 1
                  maxLength: 255
                  pattern: "^[^/\\\\\\x00]*$"
                media_type:
                  type: string
                  minLength: 1
                  maxLength: 120
                size_bytes:
                  type: integer
                  minimum: 1
                declared_sha256:
                  type:
                    - string
                    - "null"
                  pattern: "^[a-f0-9]{64}$"
                purpose:
                  type: string
                  maxLength: 32
                  enum:
                    - SUBJECT
                    - REFERENCE_MATERIAL
              description: "H-014 (Codex RC3 audit), closed in RC4: size_bytes is read directly off the body (not through str()) and the handler refuses its absence with 400 VALIDATION_FAILED before the reservation is made — the schema now requires it too, so a request valid for the schema is accepted by the real server."
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - upload_id
                  - artifact_id
                  - max_bytes
                  - expires_at
                properties:
                  upload_id:
                    type: string
                  artifact_id:
                    type: string
                  max_bytes:
                    type: integer
                    minimum: 1
                  expires_at:
                    type: string
                    format: date-time
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_RESULT_UNBOUND
              cause: "la clave existe y su resultado no apunta a nada · medido en RC01 routes/artifacts_staged.ts"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: PAYLOAD_TOO_LARGE
              cause: "el tamaño declarado excede el límite de la reserva · medido en routes/artifacts_staged.ts:88"
      x-verify-permission: "OWNER u OPERATOR · artifacts.upload"
      x-verify-permission-capability: artifacts.upload
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: artifacts.upload
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/projects/{projectId}/artifacts":
    post:
      operationId: artifacts.upload
      summary: "Bytes en staging. NO publica: la visibilidad la da artifacts.complete"
      tags:
        - artifacts
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              description: "multipart/form-data: one file part (required) plus optional plain fields transfer_sha256 and origin_detail, read by uploads.ts's own readOneFile() — not JSON, so this schema describes the multipart fields rather than a JSON body."
              type: object
              additionalProperties: false
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                transfer_sha256:
                  type: string
                  pattern: "^[a-f0-9]{64}$"
                origin_detail:
                  type: string
                  maxLength: 500
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - sha256
                  - size
                  - original_name
                  - transfer_digest_status
                  - received
                  - notice
                properties:
                  id:
                    type: string
                  sha256:
                    type: string
                    pattern: "^[a-f0-9]{64}$"
                  size:
                    type: integer
                    minimum: 0
                  original_name:
                    type: string
                  transfer_digest_status:
                    type: string
                    enum:
                      - NOT_SUPPLIED
                      - MATCH
                  received:
                    type: boolean
                    enum:
                      - true
                  notice:
                    type: string
                description: "transfer_digest_status's third DB-level value, MISMATCH, can never reach this response: the handler throws 400 TRANSFER_DIGEST_MISMATCH before building it, so a successful response only ever carries NOT_SUPPLIED or MATCH."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: PROJECT_ARCHIVED
              cause: "el proyecto está archivado: no admite evaluaciones nuevas · medido en routes/assessments.ts:31"
            - code: TRANSFER_DIGEST_MALFORMED
              cause: "el sha256 declarado en la cabecera de la petición no tiene forma hexadecimal de 64 caracteres · medido en routes/uploads.ts:68"
            - code: TRANSFER_DIGEST_MISMATCH
              cause: "el sha256 declarado no coincide con el medido en los bytes recibidos · medido en routes/uploads.ts:72"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: FILE_EXCEEDS_LIMIT
              cause: "el archivo del multipart excede el límite o llega truncado · medido en routes/uploads.ts:36,39"
        415:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: UNSUPPORTED_MEDIA_TYPE
              cause: tipo de contenido no admitido
      x-verify-permission: "OWNER u OPERATOR · artifacts.upload"
      x-verify-permission-capability: artifacts.upload
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: artifacts.upload
  "/api/v1/spaces/{spaceId}/assessments/{assessmentId}/attempts":
    get:
      operationId: assessments.attempts
      summary: "Intentos, del más reciente"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: assessmentId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - attempt_id
                        - assessment_id
                        - generation
                        - state
                        - lease_until
                        - started_at
                        - finished_at
                        - cause
                      properties:
                        attempt_id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        assessment_id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        generation:
                          type: integer
                          minimum: 1
                        state:
                          type: string
                          enum:
                            - CLAIMED
                            - RUNNING
                            - PUBLISHED
                            - FENCED_OUT
                            - ERRORED
                            - LEASE_EXPIRED
                        lease_until:
                          type:
                            - string
                            - "null"
                          format: date-time
                        started_at:
                          type: string
                          format: date-time
                        finished_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        cause:
                          type:
                            - string
                            - "null"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · assessments.read"
      x-verify-permission-capability: assessments.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.read
      x-verify-pagination:
        filters:
          - state
        order: "(started_at DESC, attempt_id DESC)"
  "/api/v1/spaces/{spaceId}/assessments/{assessmentId}/cancel":
    post:
      operationId: assessments.cancel
      summary: "Petición registrada. Si el cálculo ya terminó, el fence impide publicarlo como vigente"
      tags:
        - assessments
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: assessmentId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - job_status
                  - attempt
                  - comparison_status
                  - security_assessment_status
                  - created_at
                  - completed_at
                  - cancellation
                  - not_claimed
                properties:
                  id:
                    type: string
                  job_status:
                    type: string
                    enum:
                      - CANCELLED
                  attempt:
                    type: integer
                    description: "assessments.attempt has no DB CHECK constraint — plain INTEGER, default 1."
                  comparison_status:
                    type: string
                    description: "assessments.comparison_status has no DB CHECK constraint — plain TEXT NOT NULL, default 'NOT_RUN'."
                  security_assessment_status:
                    type:
                      - string
                      - "null"
                  created_at:
                    type: string
                    format: date-time
                  completed_at:
                    type: string
                    format: date-time
                    description: "Nullable at rest, but this handler always sets it to `now` in the same transaction before this response is built."
                  cancellation:
                    type: string
                    enum:
                      - REQUESTED_AND_RECORDED
                  not_claimed:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ASSESSMENT_NOT_CANCELLABLE
              cause: "la evaluación ya tiene un resultado escrito: terminal, no se cancela · medido en routes/retention.ts:391"
      x-verify-permission: "OWNER u OPERATOR · assessments.cancel"
      x-verify-permission-capability: assessments.cancel
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/projects/{projectId}/compare":
    get:
      operationId: assessments.compare
      summary: "Última comparación de identidad del proyecto, con la revisión exacta comparada"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - a
                  - b
                  - differences
                  - comparable
                  - note
                  - never_claimed
                properties:
                  a:
                    $ref: "#/components/schemas/AssessmentResult"
                  b:
                    $ref: "#/components/schemas/AssessmentResult"
                  differences:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - field
                        - a
                        - b
                      properties:
                        field:
                          type: string
                        a: {}
                        b: {}
                  comparable:
                    type: boolean
                  note:
                    type: string
                  never_claimed:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: TWO_ASSESSMENTS_REQUIRED
              cause: "comparar exige dos evaluaciones y falta alguna · medido en routes/assessments.ts:251"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ASSESSMENT_NOT_AVAILABLE
              cause: "evaluación no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:140"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · assessments.read"
      x-verify-permission-capability: assessments.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.read
  "/api/v1/spaces/{spaceId}/projects/{projectId}/assessments":
    post:
      operationId: assessments.create
      summary: "Aceptada y encolada. 202, no 201: el resultado aún no existe, y este modelo no puede transportarlo"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssessmentRequest"
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - assessment
                  - reused
                properties:
                  assessment:
                    $ref: "#/components/schemas/AssessmentResult"
                  reused:
                    type: boolean
                description: "H-005/3c correction (VERIFY 1.1 RC2): the generator's own special case used to attach a bare AssessmentResult here, omitting the real wrapper and the 'reused' field the handler always sends."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: EVIDENCE_NOT_INTERPRETABLE
              cause: "la evidencia no se pudo interpretar en su formato declarado · medido en routes/assessments.ts:78"
            - code: EVIDENCE_WRONG_FORMAT
              cause: "la evidencia no es del formato que este requisito exige · medido en routes/assessments.ts:77"
            - code: MODE_UNKNOWN
              cause: "RC2 §4 #1 (Codex 2026-10-10), closed in RC3: mode no es 'identity' ni 'bundle' ni ausente — ya no se pliega en silencio a null · medido en routes/drafts.ts:34"
            - code: PROJECT_ARCHIVED
              cause: "el proyecto está archivado: no admite evaluaciones nuevas · medido en routes/assessments.ts:31"
            - code: REFERENCE_NOT_APPROVED
              cause: "sólo se evalúa contra una referencia aprobada · medido en routes/assessments.ts:67"
            - code: REFERENCE_REQUIRED
              cause: "este modo de evaluación exige una referencia y no se indicó ninguna · medido en routes/assessments.ts:61"
            - code: REQUIREMENTS_INCOMPATIBLE_WITH_MODE
              cause: "la definición de requisitos invoca el núcleo y este modo no lo admite · medido en routes/assessments.ts:57"
            - code: REQUIREMENTS_NOT_APPROVED
              cause: "sólo se evalúa contra un requisito aprobado · medido en routes/assessments.ts:49"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ARTIFACT_NOT_AVAILABLE
              cause: "artefacto no visible para esta identidad, incluido el de otro espacio · medido en routes/assessments.ts:42"
            - code: EVIDENCE_NOT_AVAILABLE
              cause: "evidencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:76"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: REFERENCE_NOT_AVAILABLE
              cause: "referencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:66"
            - code: REQUIREMENTS_NOT_AVAILABLE
              cause: "requisito no visible para esta identidad, incluido el de otro espacio · medido en routes/assessments.ts:48"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_KEY_REUSED_WITH_DIFFERENT_INPUT
              cause: "la misma clave con otra entrada · medido en RC01 routes/assessments.ts:100"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: KERNEL_NOT_READY
              cause: "el núcleo de evaluación hace falta y no está disponible · medido en routes/assessments.ts:81"
      x-verify-permission: "OWNER u OPERATOR · assessments.run"
      x-verify-permission-capability: assessments.run
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.run
      x-verify-idempotency-scope: "(space_id, operationId, clave). La captura de entradas fija la identidad del trabajo: misma clave con otras entradas es 409"
  "/api/v1/spaces/{spaceId}/assessments/{assessmentId}":
    get:
      operationId: assessments.get
      summary: "Evaluación con sus reglas cruzadas"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: assessmentId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - assessment
                  - input_snapshot
                  - worker_profile
                  - project_id
                  - created_at
                properties:
                  assessment:
                    $ref: "#/components/schemas/AssessmentResult"
                  input_snapshot:
                    type:
                      - object
                      - "null"
                  worker_profile:
                    type:
                      - object
                      - "null"
                  project_id:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                description: "H-005/3c correction (VERIFY 1.1 RC2): the generator's own special case used to attach a bare AssessmentResult here, omitting the real wrapper (input_snapshot, worker_profile, project_id, created_at) the handler (assessments.ts) always sends alongside it."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ASSESSMENT_NOT_AVAILABLE
              cause: "evaluación no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:140"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · assessments.read"
      x-verify-permission-capability: assessments.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.read
  "/api/v1/spaces/{spaceId}/assessments":
    get:
      operationId: assessments.list
      summary: "Evaluaciones del espacio, sin resultado embebido"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - project_id
                        - mode
                        - job_status
                        - comparison_status
                        - record_valid
                        - security_assessment_status
                        - completeness
                        - created_at
                        - evaluated_at
                      properties:
                        id:
                          type: string
                        project_id:
                          type: string
                        mode:
                          type: string
                          enum:
                            - identity
                            - bundle
                        job_status:
                          type: string
                          enum:
                            - DRAFT
                            - VALIDATING
                            - NEEDS_INPUT
                            - QUEUED
                            - RUNNING
                            - COMPLETED
                            - ERROR
                            - INTERRUPTED
                            - CANCELLED
                        comparison_status:
                          type: string
                          enum:
                            - MATCH
                            - MISMATCH
                            - NO_REFERENCE
                            - NOT_RUN
                        record_valid:
                          type:
                            - boolean
                            - "null"
                        security_assessment_status:
                          type:
                            - string
                            - "null"
                          enum:
                            - PASS
                            - FAIL
                            - NO_CONSTA
                            - null
                        completeness:
                          type: string
                          enum:
                            - COMPLETE
                            - INCOMPLETE
                            - NOT_APPLICABLE
                        created_at:
                          type: string
                          format: date-time
                        evaluated_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · assessments.read"
      x-verify-permission-capability: assessments.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.read
      x-verify-pagination:
        filters:
          - project_id
          - job_status
          - mode
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/assessments/{assessmentId}/retry":
    post:
      operationId: assessments.retry
      summary: "Intento nuevo con generación nueva; sin resultado embebido"
      tags:
        - assessments
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: assessmentId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - assessment
                  - retried_from
                  - attempt
                properties:
                  assessment:
                    $ref: "#/components/schemas/AssessmentResult"
                  retried_from:
                    type: string
                  attempt:
                    type: integer
                    minimum: 1
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ASSESSMENT_NOT_AVAILABLE
              cause: "evaluación no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:140"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: COMPLETED_RESULTS_ARE_IMMUTABLE
              cause: "un resultado terminado no se reintenta · medido en RC01 routes/assessments.ts:211"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: KERNEL_NOT_READY
              cause: "el núcleo de evaluación hace falta y no está disponible · medido en routes/assessments.ts:81"
      x-verify-permission: "OWNER u OPERATOR · assessments.run"
      x-verify-permission-capability: assessments.run
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: assessments.run
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  /api/v1/auth/invitations/inspect:
    post:
      operationId: auth.invitationInspect
      summary: Describe sin consumir
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - token
              properties:
                token:
                  type: string
                  maxLength: 512
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - state
                properties:
                  state:
                    type: string
                    enum:
                      - INVALID
                      - VALID
                      - REVOKED
                      - USED
                      - EXPIRED
                  space_name:
                    type:
                      - string
                      - "null"
                  role:
                    type:
                      - string
                      - "null"
                    enum:
                      - OWNER
                      - OPERATOR
                      - VIEWER
                      - null
                  email:
                    type:
                      - string
                      - "null"
                  delivery:
                    type: string
                    enum:
                      - OWNER_MANUAL
                  email_verified:
                    type: boolean
                    enum:
                      - false
                description: "When state is INVALID (no row for the token at all) the handler sends ONLY {state:'INVALID'} — none of the other five keys are present. The other four states always send all six. Both shapes satisfy this one schema because only 'state' is required."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        429:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: RATE_LIMITED
              cause: "se superó el límite de peticiones de la ventana. Lo emite el manejador del framework, no la operación"
      x-verify-permission: "público, con cuota"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/invitations/redeem:
    post:
      operationId: auth.invitationRedeem
      summary: "Redención atómica"
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - token
                - display_name
                - password
              properties:
                token:
                  type: string
                  maxLength: 512
                display_name:
                  type: string
                  maxLength: 120
                password:
                  type: string
                  minLength: 15
                  maxLength: 128
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - ok
                  - space_id
                  - role
                  - csrf_token
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  space_id:
                    type: string
                  role:
                    type: string
                    enum:
                      - OWNER
                      - OPERATOR
                      - VIEWER
                  csrf_token:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: INVITATION_NOT_USABLE
              cause: "la invitación está revocada, ya usada, o caducada · medido en routes/auth.ts:223"
            - code: PASSWORD_INVALID
              cause: "validatePasswordPolicy(): password no es una cadena · medido en auth/password.ts:38"
            - code: PASSWORD_TOO_SHORT
              cause: "validatePasswordPolicy(): menos que el mínimo de caracteres declarado · medido en auth/password.ts:42"
            - code: PASSWORD_TOO_LONG
              cause: "validatePasswordPolicy(): más que el máximo de caracteres declarado · medido en auth/password.ts:43"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INVALID_CREDENTIALS
              cause: "H-001: el correo de la invitación ya es de una cuenta existente y la contraseña enviada no es la contraseña ACTUAL de esa cuenta (o la cuenta está desactivada) — misma respuesta genérica que auth.login · medido en routes/auth.ts:257"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: INVITATION_INVALID
              cause: "el token no corresponde a ninguna invitación · medido en routes/auth.ts:221"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDENTITY_CHANGED_DURING_REDEMPTION
              cause: "entre la comprobación de identidad y la escritura, la cuenta a la que apunta ahora el correo de la invitación no es la misma que se probó momentos antes · medido en routes/auth.ts:310"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        429:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: RATE_LIMITED
              cause: "se superó el límite de peticiones de la ventana. Lo emite el manejador del framework, no la operación"
      x-verify-permission: "público, con cuota"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/login:
    post:
      operationId: auth.login
      summary: "Cookie de sesión Secure + HttpOnly + host-only en Set-Cookie"
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
                - password
              properties:
                email:
                  type: string
                  maxLength: 254
                password:
                  type: string
                  maxLength: 128
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - user
                  - csrf_token
                properties:
                  user:
                    type: object
                    additionalProperties: false
                    required:
                      - id
                      - display_name
                      - email
                      - is_instance_owner
                    properties:
                      id:
                        type: string
                      display_name:
                        type: string
                      email:
                        type: string
                      is_instance_owner:
                        type: boolean
                  csrf_token:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INVALID_CREDENTIALS
              cause: "RC5 (Codex RC4 audit H-013): el email no existe, la cuenta está desactivada, o la contraseña no es la correcta — las tres respuestas idénticas, mismo coste de derivación (H-014) · medido en routes/auth.ts:87"
        429:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: RATE_LIMITED
              cause: "se superó el límite de peticiones de la ventana. Lo emite el manejador del framework, no la operación"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: SITE_IN_PORTADA
              cause: "RC5 (Codex RC4 audit H-013): con portada activa, cualquier intento que no sea el Owner acertando su propia contraseña responde esto — ni 'cuenta inexistente' ni 'contraseña incorrecta' se distinguen de 'portada activa' · medido en routes/auth.ts:79"
      x-verify-permission: "público, con cuota"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/logout:
    post:
      operationId: auth.logout
      summary: "Sesión cerrada"
      tags:
        - auth
      security:
        - sessionCookie: []
      responses:
        204:
          description: "Done, with no body."
          x-verify-success-kind: none
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: LOGOUT_REQUIRES_SESSION
              cause: "un token no tiene sesión que cerrar · medido en routes/auth.ts:136"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
      x-verify-permission: "sesión propia"
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  /api/v1/auth/me:
    get:
      operationId: auth.me
      summary: Lo que esta identidad puede ver ahora
      tags:
        - auth
      security:
        - sessionCookie: []
        - bearerToken: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - user
                  - credential
                  - spaces
                  - installation
                  - site_mode
                properties:
                  user:
                    type: object
                    additionalProperties: false
                    required:
                      - id
                      - display_name
                      - email
                      - is_instance_owner
                    properties:
                      id:
                        type: string
                      display_name:
                        type: string
                      email:
                        type: string
                      is_instance_owner:
                        type: boolean
                  credential:
                    type: object
                    additionalProperties: false
                    required:
                      - kind
                      - scopes
                      - space_id
                      - project_id
                    properties:
                      kind:
                        type: string
                        enum:
                          - session
                          - token
                      scopes:
                        type:
                          - array
                          - "null"
                        items:
                          type: string
                      space_id:
                        type:
                          - string
                          - "null"
                      project_id:
                        type:
                          - string
                          - "null"
                  spaces:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - kind
                        - role
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        kind:
                          type: string
                          enum:
                            - CLIENT
                            - DEMO
                        role:
                          type: string
                          enum:
                            - OWNER
                            - OPERATOR
                            - VIEWER
                  installation:
                    type: object
                    additionalProperties: false
                    required:
                      - origin_is_local
                      - deployment
                    properties:
                      origin_is_local:
                        type: boolean
                        enum:
                          - true
                      deployment:
                        type: string
                        enum:
                          - NOT_DEPLOYED_BY_THIS_INSTALLATION
                  site_mode:
                    type:
                      - string
                      - "null"
                    enum:
                      - OPEN
                      - PORTADA
                      - null
                    description: "null for anyone but the Owner (auth.ts: 'the mode is not theirs to see here, and they cannot reach the real endpoint anyway')."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
      x-verify-permission: identidad propia
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN_NO_SCOPE
  /api/v1/auth/site-mode-banner:
    get:
      operationId: auth.siteModeBanner
      summary: "Si el visitante actual es el Owner, y el modo portada está activo"
      tags:
        - auth
      security: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - mode
                properties:
                  mode:
                    type:
                      - string
                      - "null"
                    enum:
                      - OPEN
                      - PORTADA
                      - null
                    description: "null for anyone but the instance Owner, and for the Owner whenever the mode is OPEN rather than PORTADA — the banner only ever renders on the one truthy case."
      x-verify-permission: "público, sin cuenta"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/sessions/revoke:
    post:
      operationId: auth.revokeSessions
      summary: "Corta en la siguiente comprobación de autorización"
      tags:
        - auth
      security:
        - sessionCookie: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - ok
                  - sessions_revoked
                  - api_tokens_revoked
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  sessions_revoked:
                    type: boolean
                    enum:
                      - true
                  api_tokens_revoked:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: REVOKE_REQUIRES_SESSION
              cause: "un token no tiene sesiones de otros dispositivos que revocar · medido en routes/auth.ts:155"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
      x-verify-permission: "sesión propia"
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  /api/v1/auth/password/change:
    post:
      operationId: auth.passwordChange
      summary: "Owner order, 2026-10-17 (RC7), pieza 5: miembros, nunca el Owner. Cierra las sesiones DE OTROS dispositivos — la que hace esta llamada sigue viva, al contrario de auth.revokeSessions."
      tags:
        - auth
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - current_password
                - new_password
              properties:
                current_password:
                  type: string
                  maxLength: 128
                new_password:
                  type: string
                  maxLength: 128
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - ok
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CHANGE_REQUIRES_SESSION
              cause: "RC7 pieza 5: un token no tiene contraseña propia que cambiar · medido en routes/auth.ts"
            - code: OWNER_PASSWORD_NOT_CHANGEABLE_HERE
              cause: "RC7 piezas 2/4/5: la contraseña del Owner nunca se cambia por esta vía · medido en routes/auth.ts"
            - code: PASSWORD_TOO_SHORT
              cause: "validatePasswordPolicy(): menos de LIMITS.passwordMinLength caracteres"
            - code: PASSWORD_TOO_LONG
              cause: "validatePasswordPolicy(): más de LIMITS.passwordMaxLength caracteres"
            - code: PASSWORD_INVALID
              cause: "validatePasswordPolicy(): el campo no es una cadena"
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: la real, literal code onlyKeys() throws for any field the body declares that this operation does not accept."
            - code: FIELD_NOT_STRING
              cause: "str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332."
            - code: FIELD_TOO_LONG
              cause: "str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
            - code: SESSION_INVALID
              cause: "RC7 pieza 5: la sesión no corresponde a ningún usuario vigente · medido en routes/auth.ts"
            - code: CURRENT_PASSWORD_INVALID
              cause: "RC7 pieza 5: la contraseña actual no verifica · medido en routes/auth.ts"
      x-verify-permission: "sesión propia, no Owner"
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  /api/v1/auth/password-reset/availability:
    get:
      operationId: auth.passwordResetAvailability
      summary: "Owner order, 2026-10-17 (RC7), pieza 4: una señal global, sin cuenta — cero riesgo de enumeración porque no nombra ninguna cuenta. El cliente la usa para decidir entre el formulario y el aviso 'pide a tu administrador un enlace'."
      tags:
        - auth
      security: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - available
                properties:
                  available:
                    type: boolean
      x-verify-permission: "público, sin cuenta"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/password-reset/request:
    post:
      operationId: auth.passwordResetRequest
      summary: "Owner order, 2026-10-17 (RC7), pieza 4: resistente a enumeración por diseño — la misma respuesta si la cuenta no existe, es el Owner, no está verificada, o está limitada por tasa. Límite: 3 pedidos por correo por hora, más el límite por IP del propio rateLimit del framework."
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
              properties:
                email:
                  type: string
                  maxLength: 254
                lang:
                  type: string
                  enum:
                    - en
                    - es
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - ok
                  - notice_key
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
                  notice_key:
                    type: string
                    enum:
                      - auth.passwordResetSent
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: la real, literal code onlyKeys() throws for any field the body declares that this operation does not accept."
            - code: FIELD_NOT_STRING
              cause: "str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332."
            - code: FIELD_TOO_LONG
              cause: "str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano."
        429:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: RATE_LIMITED
              cause: "se superó el límite de peticiones de la ventana por IP. Lo emite el manejador del framework, no la operación"
      x-verify-permission: "público, con cuota"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/auth/password-reset/confirm:
    post:
      operationId: auth.passwordResetConfirm
      summary: "Owner order, 2026-10-17 (RC7), pieza 4: redime un token SELF_SERVICE u OWNER_BACKUP por el mismo camino — expirado y ya usado responden lo mismo, nunca una razón que distinga cuáles tokens existen."
      tags:
        - auth
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - token
                - password
              properties:
                token:
                  type: string
                  maxLength: 512
                password:
                  type: string
                  maxLength: 256
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - ok
                properties:
                  ok:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: RESET_TOKEN_NOT_USABLE
              cause: "RC7 pieza 4: el token no existe, ya se usó o expiró — misma respuesta en los tres casos · medido en routes/password_reset.ts"
            - code: PASSWORD_TOO_SHORT
              cause: "validatePasswordPolicy(): menos de LIMITS.passwordMinLength caracteres"
            - code: PASSWORD_TOO_LONG
              cause: "validatePasswordPolicy(): más de LIMITS.passwordMaxLength caracteres"
            - code: PASSWORD_INVALID
              cause: "validatePasswordPolicy(): el campo no es una cadena"
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: la real, literal code onlyKeys() throws for any field the body declares that this operation does not accept."
            - code: FIELD_NOT_STRING
              cause: "str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332."
            - code: FIELD_TOO_LONG
              cause: "str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano."
      x-verify-permission: "público, un solo uso"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  "/api/v1/spaces/{spaceId}/connections":
    post:
      operationId: connections.create
      summary: "Conexión registrada"
      tags:
        - connections
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - type
                - name
              properties:
                type:
                  type: string
                  maxLength: 16
                  enum:
                    - FILE
                    - GITHUB
                    - CLI
                    - CI
                    - AGENT
                name:
                  type: string
                  maxLength: 120
                project_id:
                  type:
                    - string
                    - "null"
                repository:
                  type: string
                  maxLength: 220
                token:
                  type: string
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - type
                  - configuration_status
                  - validation_status
                  - public_configuration
                properties:
                  id:
                    type: string
                  type:
                    type: string
                    enum:
                      - FILE
                      - GITHUB
                      - CLI
                      - CI
                      - AGENT
                  configuration_status:
                    type: string
                    enum:
                      - NOT_CONFIGURED
                      - CONFIGURED
                      - REVOKED
                  validation_status:
                    type: string
                    enum:
                      - NOT_TESTED
                  public_configuration:
                    type: object
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: CONNECTION_TYPE_UNKNOWN
              cause: "el tipo de conexión no está en la lista admitida · medido en routes/connections.ts:92"
            - code: REPOSITORY_FORMAT_INVALID
              cause: "el repositorio no tiene la forma owner/repo · medido en routes/connections.ts:110"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · connections.configure"
      x-verify-permission-capability: connections.configure
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: connections.list
      summary: Conexiones del espacio
      tags:
        - connections
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                  - available_types
                  - note
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - project_id
                        - type
                        - name
                        - implementation_status
                        - configuration_status
                        - validation_status
                        - last_tested_at
                        - last_test_detail
                        - created_at
                        - public_configuration
                        - descriptor
                      properties:
                        id:
                          type: string
                        project_id:
                          type:
                            - string
                            - "null"
                        type:
                          type: string
                          enum:
                            - FILE
                            - GITHUB
                            - CLI
                            - CI
                            - AGENT
                        name:
                          type: string
                        implementation_status:
                          type: string
                          enum:
                            - IMPLEMENTED
                            - SPEC
                            - PLANNED
                        configuration_status:
                          type: string
                          enum:
                            - NOT_CONFIGURED
                            - CONFIGURED
                            - REVOKED
                        validation_status:
                          type: string
                          enum:
                            - NOT_TESTED
                            - LOCAL_CONTRACT_TESTED
                            - LIVE_TESTED
                            - FAILED
                        last_tested_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        last_test_detail:
                          type: string
                        created_at:
                          type: string
                          format: date-time
                        public_configuration:
                          type: object
                          description: "Shape depends on connection type (connections.ts's DESCRIPTORS/branches) and is not itself enforced by any schema in the handler — left loose rather than inventing a per-type structure nothing checks."
                        descriptor:
                          type: object
                          additionalProperties: false
                          required:
                            - collects
                            - does_not
                            - implementation
                          properties:
                            collects:
                              type: string
                            does_not:
                              type: string
                            implementation:
                              type: string
                              enum:
                                - IMPLEMENTED
                                - SPEC
                                - PLANNED
                  page:
                    $ref: "#/components/schemas/PageInfo"
                  available_types:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - type
                        - collects
                        - does_not
                        - implementation
                      properties:
                        type:
                          type: string
                          enum:
                            - FILE
                            - GITHUB
                            - CLI
                            - CI
                            - AGENT
                        collects:
                          type: string
                        does_not:
                          type: string
                        implementation:
                          type: string
                          enum:
                            - IMPLEMENTED
                            - SPEC
                            - PLANNED
                  note:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
      x-verify-pagination:
        filters:
          - provider
          - state
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/connections/{connectionId}":
    get:
      operationId: connections.get
      summary: "Conexión"
      tags:
        - connections
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - connection
                  - activity
                  - recent_events_scope
                  - limits
                  - secret_returned
                properties:
                  connection:
                    type: object
                    additionalProperties: false
                    required:
                      - id
                      - project_id
                      - type
                      - name
                      - implementation_status
                      - configuration_status
                      - validation_status
                      - last_tested_at
                      - last_test_detail
                      - created_at
                      - public_configuration
                      - descriptor
                    properties:
                      id:
                        type: string
                      project_id:
                        type:
                          - string
                          - "null"
                      type:
                        type: string
                        enum:
                          - FILE
                          - GITHUB
                          - CLI
                          - CI
                          - AGENT
                      name:
                        type: string
                      implementation_status:
                        type: string
                        enum:
                          - IMPLEMENTED
                          - SPEC
                          - PLANNED
                      configuration_status:
                        type: string
                        enum:
                          - NOT_CONFIGURED
                          - CONFIGURED
                          - REVOKED
                      validation_status:
                        type: string
                        enum:
                          - NOT_TESTED
                          - LOCAL_CONTRACT_TESTED
                          - LIVE_TESTED
                          - FAILED
                      last_tested_at:
                        type:
                          - string
                          - "null"
                        format: date-time
                      last_test_detail:
                        type: string
                      created_at:
                        type: string
                        format: date-time
                      public_configuration:
                        type: object
                        description: "Shape depends on connection type (connections.ts's DESCRIPTORS/branches) and is not itself enforced by any schema in the handler — left loose rather than inventing a per-type structure nothing checks."
                      descriptor:
                        type: object
                        additionalProperties: false
                        required:
                          - collects
                          - does_not
                          - implementation
                        properties:
                          collects:
                            type: string
                          does_not:
                            type: string
                          implementation:
                            type: string
                            enum:
                              - IMPLEMENTED
                              - SPEC
                              - PLANNED
                  activity:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - action
                        - outcome
                        - occurred_at
                        - metadata
                      properties:
                        id:
                          type: string
                        action:
                          type: string
                        outcome:
                          type: string
                          enum:
                            - OK
                            - DENIED
                            - ERROR
                        occurred_at:
                          type: string
                          format: date-time
                        metadata:
                          type: object
                          description: "RC2 §4 #4 (Codex 2026-10-10), closed in RC3: parsed the same way events.list parses it, no longer sent as the raw JSON-encoded string."
                  recent_events_scope:
                    type: object
                    additionalProperties: false
                    required:
                      - class
                      - shown
                      - view_limit
                      - more_exist
                      - complete
                      - full_record
                      - note
                    properties:
                      class:
                        type: string
                        enum:
                          - BOUNDED_SUBSET
                      shown:
                        type: integer
                        minimum: 0
                      view_limit:
                        type: integer
                        enum:
                          - 20
                      more_exist:
                        type: boolean
                      complete:
                        type: boolean
                      full_record:
                        type: string
                      note:
                        type: string
                  limits:
                    type: object
                    additionalProperties: false
                    required:
                      - artifact_max_bytes
                      - evidence_max_bytes
                      - note
                    properties:
                      artifact_max_bytes:
                        type: integer
                        enum:
                          - 33554432
                      evidence_max_bytes:
                        type: integer
                        enum:
                          - 8388608
                      note:
                        type: string
                  secret_returned:
                    type: boolean
                    enum:
                      - false
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: CONNECTION_NOT_AVAILABLE
              cause: "conexión no visible para esta identidad, incluida la de otro espacio · medido en routes/connections.ts:173"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
  "/api/v1/spaces/{spaceId}/connections/{connectionId}/revoke":
    post:
      operationId: connections.revoke
      summary: "Conexión revocada"
      tags:
        - connections
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - configuration_status
                properties:
                  id:
                    type: string
                  configuration_status:
                    type: string
                    enum:
                      - REVOKED
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: CONNECTION_NOT_AVAILABLE
              cause: "conexión no visible para esta identidad, incluida la de otro espacio · medido en routes/connections.ts:173"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER · connections.configure"
      x-verify-permission-capability: connections.configure
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/connections/{connectionId}/test":
    post:
      operationId: connections.test
      summary: "Comprobación efectuada contra el proveedor"
      tags:
        - connections
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                description: "H-005 residual note: this response is genuinely polymorphic — its exact shape depends on the connection's type and, for GITHUB, on whether the live/double call succeeded. connections.ts is not changed to fit one shape; these five branches are the real ones, reported as-is. Worth flagging as a separate finding: a client cannot know which branch it got except by checking which optional fields are present."
                oneOf:
                  - type: object
                    additionalProperties: false
                    required:
                      - validation_status
                      - detail
                      - configuration_status
                      - live
                    properties:
                      validation_status:
                        type: string
                        enum:
                          - NOT_TESTED
                      detail:
                        type: string
                      configuration_status:
                        type: string
                        enum:
                          - NOT_CONFIGURED
                          - CONFIGURED
                          - REVOKED
                      live:
                        type: boolean
                        enum:
                          - false
                    description: "GITHUB, missing repository or credential."
                  - type: object
                    additionalProperties: false
                    required:
                      - validation_status
                      - detail
                      - repository_id
                      - full_name
                      - default_branch
                      - live
                    properties:
                      validation_status:
                        type: string
                        enum:
                          - LIVE_TESTED
                          - LOCAL_CONTRACT_TESTED
                      detail:
                        type: string
                      repository_id: {}
                      full_name:
                        type: string
                      default_branch:
                        type: string
                      live:
                        type: boolean
                    description: "GITHUB, the provider (or its local double) answered."
                  - type: object
                    additionalProperties: false
                    required:
                      - validation_status
                      - detail
                      - error
                    properties:
                      validation_status:
                        type: string
                        enum:
                          - FAILED
                      detail:
                        type: string
                      error:
                        type: object
                        additionalProperties: false
                        required:
                          - code
                          - status
                          - message
                        properties:
                          code:
                            type: string
                          status:
                            type:
                              - integer
                              - "null"
                          message:
                            type: string
                    description: "GITHUB, the provider refused or was unreachable."
                  - type: object
                    additionalProperties: false
                    required:
                      - validation_status
                      - detail
                      - base_url
                    properties:
                      validation_status:
                        type: string
                        enum:
                          - LOCAL_CONTRACT_TESTED
                      detail:
                        type: string
                      base_url:
                        type: string
                    description: "CLI, CI or AGENT."
                  - type: object
                    additionalProperties: false
                    required:
                      - validation_status
                      - detail
                    properties:
                      validation_status:
                        type: string
                        enum:
                          - LOCAL_CONTRACT_TESTED
                      detail:
                        type: string
                    description: FILE.
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: CONNECTION_NOT_AVAILABLE
              cause: "conexión no visible para esta identidad, incluida la de otro espacio · medido en routes/connections.ts:173"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: CONNECTION_REVOKED
              cause: "la conexión está revocada · medido en RC01 routes/connections.ts:195"
      x-verify-permission: "OWNER · connections.configure"
      x-verify-permission-capability: connections.configure
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/deletion-requests/{deletionId}/cancel":
    post:
      operationId: deletion.cancel
      summary: Solicitud anulada
      tags:
        - deletion
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: deletionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - state
                  - scope
                  - target_id
                  - effective_after
                  - cancellable_until
                  - requested_by
                  - requested_at
                  - cancelled_at
                  - purged_at
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  state:
                    type: string
                    enum:
                      - REQUESTED
                      - CANCELLED
                      - PURGING
                      - PURGED
                    description: "deletion_requests.state CHECK constraint, apps/server/src/db/migrations.ts"
                  scope:
                    type: string
                    enum:
                      - PROJECT
                      - ASSESSMENT
                      - SPACE_DATA
                  target_id:
                    type:
                      - string
                      - "null"
                  effective_after:
                    type: string
                    format: date-time
                  cancellable_until:
                    type: string
                    format: date-time
                  requested_by:
                    type: string
                  requested_at:
                    type: string
                    format: date-time
                  cancelled_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  purged_at:
                    type:
                      - string
                      - "null"
                    format: date-time
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: DELETION_NOT_CANCELLABLE
              cause: "la solicitud no está en un estado cancelable · medido en RC01 routes/retention.ts:294"
            - code: DELETION_WINDOW_CLOSED
              cause: "la ventana para cancelar ya cerró · medido en RC01 routes/retention.ts:296"
      x-verify-permission: "OWNER · deletion.request"
      x-verify-permission-capability: deletion.request
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/deletion-requests":
    get:
      operationId: deletion.list
      summary: Solicitudes de borrado
      tags:
        - deletion
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - space_id
                        - state
                        - scope
                        - target_id
                        - effective_after
                        - cancellable_until
                        - requested_by
                        - requested_at
                        - cancelled_at
                        - purged_at
                      properties:
                        id:
                          type: string
                        space_id:
                          type: string
                        state:
                          type: string
                          enum:
                            - REQUESTED
                            - CANCELLED
                            - PURGING
                            - PURGED
                          description: "deletion_requests.state CHECK constraint, apps/server/src/db/migrations.ts"
                        scope:
                          type: string
                          enum:
                            - PROJECT
                            - ASSESSMENT
                            - SPACE_DATA
                        target_id:
                          type:
                            - string
                            - "null"
                        effective_after:
                          type: string
                          format: date-time
                        cancellable_until:
                          type: string
                          format: date-time
                        requested_by:
                          type: string
                        requested_at:
                          type: string
                          format: date-time
                        cancelled_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        purged_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER · deletion.request"
      x-verify-permission-capability: deletion.request
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-pagination:
        filters:
          - state
          - scope
        order: "(requested_at DESC, id DESC)"
    post:
      operationId: deletion.request
      summary: "Solicitud aceptada, con su punto de no retorno"
      tags:
        - deletion
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - scope
                - reason
              properties:
                scope:
                  type: string
                  maxLength: 20
                  enum:
                    - PROJECT
                    - ASSESSMENT
                    - SPACE_DATA
                target_id:
                  type:
                    - string
                    - "null"
                  maxLength: 64
                  description: "Cross-field rule this schema subset cannot express: target_id must be null exactly when scope is SPACE_DATA, non-null otherwise (retention.ts, enforced by the handler and by a DB CHECK). Reported separately, not silently dropped."
                wait_hours:
                  type: integer
                  minimum: 1
                  maximum: 720
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - state
                  - scope
                  - target_id
                  - effective_after
                  - cancellable_until
                  - requested_by
                  - requested_at
                  - cancelled_at
                  - purged_at
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  state:
                    type: string
                    enum:
                      - REQUESTED
                      - CANCELLED
                      - PURGING
                      - PURGED
                    description: "deletion_requests.state CHECK constraint, apps/server/src/db/migrations.ts"
                  scope:
                    type: string
                    enum:
                      - PROJECT
                      - ASSESSMENT
                      - SPACE_DATA
                  target_id:
                    type:
                      - string
                      - "null"
                  effective_after:
                    type: string
                    format: date-time
                  cancellable_until:
                    type: string
                    format: date-time
                  requested_by:
                    type: string
                  requested_at:
                    type: string
                    format: date-time
                  cancelled_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  purged_at:
                    type:
                      - string
                      - "null"
                    format: date-time
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: DELETION_ALREADY_REQUESTED
              cause: "ya hay una solicitud viva para este espacio · medido en RC01 routes/retention.ts:216"
            - code: IDEMPOTENCY_RESULT_UNBOUND
              cause: "la clave existe y su resultado no apunta a nada · medido en RC01 routes/retention.ts:202"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · deletion.request"
      x-verify-permission-capability: deletion.request
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/projects/{projectId}/drafts":
    post:
      operationId: drafts.create
      summary: Borrador creado
      tags:
        - drafts
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: []
              properties:
                mode:
                  type:
                    - string
                    - "null"
                  enum:
                    - identity
                    - bundle
                    - null
                  description: "RC2 §4 #1 (Codex 2026-10-10), closed in RC3: absent or null both mean 'not chosen yet' and are accepted; any other value is now a 400 MODE_UNKNOWN, no longer silently folded into null."
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - mode
                  - state
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  mode:
                    type:
                      - string
                      - "null"
                  state:
                    type: object
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: MODE_UNKNOWN
              cause: "RC2 §4 #1 (Codex 2026-10-10), closed in RC3: mode no es 'identity' ni 'bundle' ni ausente — ya no se pliega en silencio a null · medido en routes/drafts.ts:34"
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER u OPERATOR · projects.create"
      x-verify-permission-capability: projects.create
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/drafts/{draftId}":
    get:
      operationId: drafts.get
      summary: Borrador
      tags:
        - drafts
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - mode
                  - project_id
                  - state
                  - updated_at
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  mode:
                    type:
                      - string
                      - "null"
                  project_id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  state:
                    type: object
                  updated_at:
                    type: string
                    format: date-time
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: DRAFT_NOT_AVAILABLE
              cause: "borrador no visible para esta identidad, incluido el de otro espacio · medido en routes/drafts.ts:52"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
    patch:
      operationId: drafts.update
      summary: "Actualización parcial aplicada"
      tags:
        - drafts
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: draftId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: []
              properties:
                mode:
                  type:
                    - string
                    - "null"
                state:
                  type: object
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - mode
                  - state
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  mode:
                    type:
                      - string
                      - "null"
                  state:
                    type: object
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: DRAFT_STATE_TOO_LARGE
              cause: "el estado serializado excede el límite del borrador · medido en routes/drafts.ts:76"
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: DRAFT_NOT_AVAILABLE
              cause: "borrador no visible para esta identidad, incluido el de otro espacio · medido en routes/drafts.ts:52"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER u OPERATOR · projects.create"
      x-verify-permission-capability: projects.create
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/events/export":
    get:
      operationId: events.export
      summary: "Exportación de eventos como attachment CSV"
      tags:
        - events
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: binary
          content:
            application/x-ndjson:
              schema:
                type: string
                format: binary
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER · events.export"
      x-verify-permission-capability: events.export
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/events":
    get:
      operationId: events.list
      summary: Eventos del espacio
      tags:
        - events
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                  - integrity_note
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - action
                        - object_type
                        - object_id
                        - outcome
                        - occurred_at
                        - metadata
                        - actor_name
                      properties:
                        id:
                          type: string
                        action:
                          type: string
                        object_type:
                          type: string
                        object_id:
                          type:
                            - string
                            - "null"
                        outcome:
                          type: string
                          enum:
                            - OK
                            - DENIED
                            - ERROR
                        occurred_at:
                          type: string
                          format: date-time
                        metadata:
                          type: object
                        actor_name:
                          type:
                            - string
                            - "null"
                  page:
                    $ref: "#/components/schemas/PageInfo"
                  integrity_note:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · events.read"
      x-verify-permission-capability: events.read
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-pagination:
        filters:
          - action
          - object_kind
          - from
          - to
        order: "(at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/events/subjects":
    get:
      operationId: events.subjects
      summary: "RC11, H-030: el nombre de una persona o de un token ya no viaja en events.metadata_json (append-only, sin purgado). Esta operación resuelve, en vivo, el correo de una invitación, el nombre de quien tiene una membresía o el nombre de un token, por el mismo id que el evento ya declaraba — null si esa fila ya no existe en este espacio."
      tags:
        - events
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - subjects
                properties:
                  subjects:
                    type: object
                    description: "Keyed by the object_id each caller asked about (at most 100 per request, deduplicated). A value is the current email_display of an invitation, the current display_name of the user behind a membership, or the current name of an api_token — whichever the id's own prefix (inv_/mem_/tok_) names — or null when that row is no longer in this space."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · events.read"
      x-verify-permission-capability: events.read
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/evidence/{evidenceId}/download":
    get:
      operationId: evidence.download
      summary: Bytes originales de la evidencia
      tags:
        - evidence
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: evidenceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: binary
          content:
            application/octet-stream:
              schema:
                type: string
                format: binary
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: EVIDENCE_BYTES_MISSING
              cause: "la fila de evidencia existe y sus bytes no están en el almacén · medido en routes/uploads.ts:211"
            - code: EVIDENCE_NOT_AVAILABLE
              cause: "evidencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:76"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
  "/api/v1/spaces/{spaceId}/evidence/{evidenceId}":
    get:
      operationId: evidence.get
      summary: Evidencia
      tags:
        - evidence
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: evidenceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - project_id
                  - original_name
                  - binary_sha256
                  - size
                  - format
                  - adapter_version
                  - parse_status
                  - parse_detail
                  - source_assurance
                  - subject_sha256
                  - run_id
                  - produced_at
                  - uploaded_at
                properties:
                  id:
                    type: string
                  project_id:
                    type: string
                  original_name:
                    type: string
                  binary_sha256:
                    type: string
                    pattern: "^[a-f0-9]{64}$"
                  size:
                    type: integer
                    minimum: 0
                  format:
                    type: string
                    enum:
                      - MFAK-BUNDLE-2.0
                      - AC7-PINS
                      - ATTACHMENT
                  adapter_version:
                    type: string
                  parse_status:
                    type: string
                    enum:
                      - SUPPORTED
                      - INTERPRETATION_UNSUPPORTED
                      - INVALID
                  parse_detail:
                    type: string
                  source_assurance:
                    type: string
                    enum:
                      - LOCAL_MEASUREMENT
                      - IMPORTED_ASSERTION
                      - REFERENCE_FIXTURE
                      - UNINTERPRETED_ATTACHMENT
                  subject_sha256:
                    type:
                      - string
                      - "null"
                  run_id:
                    type:
                      - string
                      - "null"
                  produced_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  uploaded_at:
                    type: string
                    format: date-time
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: EVIDENCE_NOT_AVAILABLE
              cause: "evidencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:76"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
  "/api/v1/spaces/{spaceId}/projects/{projectId}/evidence":
    get:
      operationId: evidence.list
      summary: Evidencia del proyecto
      tags:
        - evidence
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - original_name
                        - binary_sha256
                        - size
                        - format
                        - parse_status
                        - parse_detail
                        - source_assurance
                        - subject_sha256
                        - run_id
                        - produced_at
                        - uploaded_at
                      properties:
                        id:
                          type: string
                        original_name:
                          type: string
                        binary_sha256:
                          type: string
                          pattern: "^[a-f0-9]{64}$"
                        size:
                          type: integer
                          minimum: 0
                        format:
                          type: string
                          enum:
                            - MFAK-BUNDLE-2.0
                            - AC7-PINS
                            - ATTACHMENT
                        parse_status:
                          type: string
                          enum:
                            - SUPPORTED
                            - INTERPRETATION_UNSUPPORTED
                            - INVALID
                        parse_detail:
                          type: string
                        source_assurance:
                          type: string
                          enum:
                            - LOCAL_MEASUREMENT
                            - IMPORTED_ASSERTION
                            - REFERENCE_FIXTURE
                            - UNINTERPRETED_ATTACHMENT
                        subject_sha256:
                          type:
                            - string
                            - "null"
                        run_id:
                          type:
                            - string
                            - "null"
                        produced_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        uploaded_at:
                          type: string
                          format: date-time
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
      x-verify-pagination:
        filters:
          - kind
          - interpreted
        order: "(created_at DESC, id DESC)"
    post:
      operationId: evidence.upload
      summary: Evidencia registrada
      tags:
        - evidence
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              description: "multipart/form-data: one file part (required) plus optional plain field 'format' (defaults to ATTACHMENT when absent)."
              type: object
              additionalProperties: false
              required:
                - file
              properties:
                file:
                  type: string
                  format: binary
                format:
                  type: string
                  enum:
                    - MFAK-BUNDLE-2.0
                    - AC7-PINS
                    - ATTACHMENT
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - binary_sha256
                  - size
                  - original_name
                  - format
                  - parse_status
                  - parse_detail
                  - source_assurance
                  - subject_sha256
                  - run_id
                  - produced_at
                  - adapter_version
                properties:
                  id:
                    type: string
                  binary_sha256:
                    type: string
                    pattern: "^[a-f0-9]{64}$"
                  size:
                    type: integer
                    minimum: 0
                  original_name:
                    type: string
                  format:
                    type: string
                    enum:
                      - MFAK-BUNDLE-2.0
                      - AC7-PINS
                      - ATTACHMENT
                  parse_status:
                    type: string
                    enum:
                      - SUPPORTED
                      - INTERPRETATION_UNSUPPORTED
                      - INVALID
                  parse_detail:
                    type: string
                  source_assurance:
                    type: string
                    enum:
                      - LOCAL_MEASUREMENT
                      - IMPORTED_ASSERTION
                      - REFERENCE_FIXTURE
                      - UNINTERPRETED_ATTACHMENT
                  subject_sha256:
                    type:
                      - string
                      - "null"
                  run_id:
                    type:
                      - string
                      - "null"
                  produced_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  adapter_version:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: FORMAT_UNKNOWN
              cause: "el formato declarado para la evidencia no está en la lista admitida · medido en routes/uploads.ts:134"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: FILE_EXCEEDS_LIMIT
              cause: "el archivo del multipart excede el límite o llega truncado · medido en routes/uploads.ts:36,39"
        415:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: UNSUPPORTED_MEDIA_TYPE
              cause: tipo de contenido no admitido
      x-verify-permission: "OWNER u OPERATOR · evidence.upload"
      x-verify-permission-capability: evidence.upload
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: evidence.upload
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/exports":
    post:
      operationId: exports.create
      summary: Export encolado
      tags:
        - exports
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - assessment_id
              properties:
                assessment_id:
                  type: string
                  maxLength: 128
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - assessment_id
                  - sha256
                  - size
                  - canonical_note
                  - canonical_probe
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  assessment_id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  sha256:
                    type: string
                    pattern: "^[a-f0-9]{64}$"
                  size:
                    type: integer
                    minimum: 0
                  canonical_note:
                    type: string
                  canonical_probe:
                    type: string
                    pattern: "^[a-f0-9]{64}$"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: ASSESSMENT_NOT_COMPLETED
              cause: "sólo se exporta una evaluación cuyo job_status es COMPLETED · medido en routes/exports.ts:26"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ASSESSMENT_NOT_AVAILABLE
              cause: "evaluación no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:140"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "miembro · exports.read"
      x-verify-permission-capability: exports.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: exports.read
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: exports.list
      summary: Exports del espacio
      tags:
        - exports
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - assessment_id
                        - kind
                        - manifest_digest
                        - size
                        - created_at
                        - mode
                        - project_id
                      properties:
                        id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        assessment_id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        kind:
                          type: string
                          enum:
                            - ASSESSMENT_REPORT_JSON
                        manifest_digest:
                          type: string
                          pattern: "^[a-f0-9]{64}$"
                        size:
                          type: integer
                          minimum: 0
                        created_at:
                          type: string
                          format: date-time
                        mode:
                          type: string
                          enum:
                            - identity
                            - bundle
                        project_id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · exports.read"
      x-verify-permission-capability: exports.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: exports.read
      x-verify-pagination:
        filters:
          - state
          - assessment_id
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/exports/{exportId}/download":
    get:
      operationId: exports.download
      summary: Informe como attachment
      tags:
        - exports
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: exportId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: binary
          content:
            application/json:
              schema:
                type: string
                format: binary
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: EXPORT_BYTES_MISSING
              cause: "la fila de la exportación existe y sus bytes no están en el almacén · medido en routes/exports.ts:117"
            - code: EXPORT_NOT_AVAILABLE
              cause: "exportación no visible para esta identidad, incluida la de otro espacio · medido en routes/exports.ts:114"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · exports.read"
      x-verify-permission-capability: exports.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: exports.read
  "/api/v1/spaces/{spaceId}/connections/{connectionId}/imports":
    post:
      operationId: github.import
      summary: "Importación encolada"
      tags:
        - github
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - ref
              properties:
                ref:
                  type: string
                  minLength: 1
                  maxLength: 200
                path:
                  type:
                    - string
                    - "null"
                  minLength: 1
                  maxLength: 400
                project_id:
                  type:
                    - string
                    - "null"
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - connection_id
                  - requested_ref
                  - resolved_commit
                  - state
                  - artifact_id
                  - failure_cause
                  - created_at
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  connection_id:
                    type: string
                  requested_ref:
                    type: string
                  resolved_commit:
                    type:
                      - string
                      - "null"
                  state:
                    type: string
                    enum:
                      - QUEUED
                      - RESOLVING
                      - FETCHING
                      - STORED
                      - FAILED
                  artifact_id:
                    type:
                      - string
                      - "null"
                  failure_cause:
                    type:
                      - string
                      - "null"
                  created_at:
                    type: string
                    format: date-time
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: CONNECTION_NOT_CONFIGURED
              cause: "el repositorio pedido no es el que la conexión tiene configurado · medido en routes/imports.ts:80"
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: IMPORT_PATH_REFUSED
              cause: "la ruta del repositorio no es codificable con seguridad · medido en routes/imports.ts:203"
            - code: IMPORT_PROJECT_REQUIRED
              cause: "hay más de un proyecto candidato y la importación no elige por sí sola · medido en routes/imports.ts:227"
            - code: IMPORT_REF_REFUSED
              cause: "la referencia (rama, tag o commit) no es codificable con seguridad · medido en routes/imports.ts:205"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_RESULT_UNBOUND
              cause: "la clave existe y su resultado no apunta a nada · medido en RC01 routes/connections.ts"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: REQUEST_TOO_LARGE
              cause: "el cuerpo de la petición excede el límite del servidor y la rechaza el framework antes de llegar a la operación"
      x-verify-permission: "OWNER u OPERATOR · connections.import"
      x-verify-permission-capability: connections.import
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/connections/{connectionId}/imports/{importId}":
    get:
      operationId: github.importGet
      summary: "Estado de la importación"
      tags:
        - github
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: importId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - connection_id
                  - requested_ref
                  - resolved_commit
                  - state
                  - artifact_id
                  - failure_cause
                  - created_at
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  connection_id:
                    type: string
                  requested_ref:
                    type: string
                  resolved_commit:
                    type:
                      - string
                      - "null"
                  state:
                    type: string
                    enum:
                      - QUEUED
                      - RESOLVING
                      - FETCHING
                      - STORED
                      - FAILED
                  artifact_id:
                    type:
                      - string
                      - "null"
                  failure_cause:
                    type:
                      - string
                      - "null"
                  created_at:
                    type: string
                    format: date-time
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · connections.read"
      x-verify-permission-capability: connections.read
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/connections/{connectionId}/candidates":
    get:
      operationId: github.listCandidates
      summary: Candidatos del repositorio exacto configurado
      tags:
        - github
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: connectionId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                description: "Two genuinely different shapes, both at the operation's own success_code (connections.ts/imports.ts never forced into one): a connection with no repository or no stored credential reports that fact instead of an empty page masquerading as a real one."
                oneOf:
                  - type: object
                    additionalProperties: false
                    required:
                      - items
                      - page
                      - configuration_status
                      - detail
                    properties:
                      items:
                        type: array
                        maxItems: 0
                      page:
                        $ref: "#/components/schemas/PageInfo"
                      configuration_status:
                        type: string
                        enum:
                          - NOT_CONFIGURED
                      detail:
                        type: string
                  - type: object
                    additionalProperties: false
                    required:
                      - items
                      - page
                      - live
                      - note
                    properties:
                      items:
                        type: array
                        items:
                          type: object
                          additionalProperties: false
                          required:
                            - ref
                            - commit
                            - updated_at
                          properties:
                            ref:
                              type: string
                            commit:
                              type: string
                            updated_at:
                              type: string
                              format: date-time
                      page:
                        $ref: "#/components/schemas/PageInfo"
                      live:
                        type: boolean
                      truncated_at:
                        type: integer
                        enum:
                          - 400
                      detail:
                        type: string
                      note:
                        type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER u OPERATOR · connections.import"
      x-verify-permission-capability: connections.import
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-pagination:
        filters:
          - kind
        order: "(updated_at DESC, ref DESC)"
  "/api/v1/spaces/{spaceId}/invitations/{invitationId}/revoke":
    post:
      operationId: invitations.revoke
      summary: "Invitación revocada"
      tags:
        - invitations
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: invitationId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - reason
              properties:
                reason:
                  type: string
                  minLength: 1
                  maxLength: 500
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - space_id
                  - role
                  - delivery
                  - email
                  - created_at
                  - expires_at
                  - used_at
                  - revoked_at
                  - state
                properties:
                  id:
                    type: string
                  space_id:
                    type: string
                  role:
                    type: string
                    enum:
                      - OWNER
                      - OPERATOR
                      - VIEWER
                  delivery:
                    type: string
                    description: "invitations.delivery has no DB CHECK constraint — plain TEXT, default 'OWNER_MANUAL'. Not given an enum because none is enforced."
                  email:
                    type: string
                  created_at:
                    type: string
                    format: date-time
                  expires_at:
                    type: string
                    format: date-time
                  used_at:
                    type: "null"
                    description: "The handler sends a literal null here always — by the time this reply is built, the row's own used_at has already been checked to be null (otherwise the request was refused with 409)."
                  revoked_at:
                    type: string
                    format: date-time
                  state:
                    type: string
                    enum:
                      - REVOKED
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INVITATION_ALREADY_REDEEMED
              cause: "la invitación ya se canjeó: es 409 y no 404 porque sigue ahí · medido en RC01 routes/retention.ts:342"
            - code: INVITATION_ALREADY_REVOKED
              cause: "la invitación ya estaba revocada · medido en RC01 routes/retention.ts:343"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  /api/v1/system/reconcile:
    get:
      operationId: maintenance.reconcileList
      summary: "Barridos, del más reciente"
      tags:
        - maintenance
      security:
        - sessionCookie: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - run_id
                        - scope
                        - dry_run
                        - state
                        - started_at
                        - finished_at
                        - scanned
                        - orphans
                        - missing_objects
                        - purged
                        - actor
                      properties:
                        run_id:
                          type: string
                        scope:
                          type: string
                        dry_run:
                          type: boolean
                        state:
                          type: string
                          enum:
                            - RUNNING
                            - FINISHED
                            - FAILED
                        started_at:
                          type: string
                          format: date-time
                        finished_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        scanned:
                          type: integer
                          minimum: 0
                        orphans:
                          type: integer
                          minimum: 0
                        missing_objects:
                          type: integer
                          minimum: 0
                        purged:
                          type: integer
                          minimum: 0
                        actor:
                          type: object
                          additionalProperties: false
                          required:
                            - kind
                            - user_id
                            - token_id
                            - display
                          properties:
                            kind:
                              type: string
                              enum:
                                - USER
                                - API_TOKEN
                                - SYSTEM
                            user_id:
                              type:
                                - string
                                - "null"
                            token_id:
                              type: "null"
                            display:
                              type: string
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
      x-verify-permission: instance owner
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
      x-verify-pagination:
        filters:
          - state
        order: "(started_at DESC, run_id DESC)"
    post:
      operationId: maintenance.reconcileRun
      summary: "Aceptado para ejecución asíncrona"
      tags:
        - maintenance
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - scope
              properties:
                scope:
                  type: string
                  minLength: 1
                  maxLength: 64
                dry_run:
                  type: boolean
                  enum:
                    - true
                  description: "The only value this handler accepts for success: dry_run:false is well-formed JSON but is refused with 400 RECONCILE_WET_RUN_NOT_IMPLEMENTED — the wet path does not exist. Reported separately rather than silently dropped from the schema."
                verify_digests:
                  type: boolean
      responses:
        202:
          description: "Accepted: the request is recorded and the work is not done yet."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - run_id
                properties:
                  run_id:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: RECONCILE_WET_RUN_NOT_IMPLEMENTED
              cause: "la purga real todavía no existe: dry_run:false se rechaza en vez de fingir que borró algo · medido en routes/maintenance.ts:95"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_RESULT_UNBOUND
              cause: "la clave existe y su resultado no apunta a nada · medido en RC01 routes/maintenance.ts"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
            - code: IDEMPOTENCY_RESULT_KIND_UNKNOWN
              cause: "la clave de idempotencia guarda un resultado de un tipo que esta operación no reconoce · medido en idempotency.ts:249"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: instance owner
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
      x-verify-idempotency-scope: "(instalación, operationId, clave)"
  /api/v1/system/backup/run:
    post:
      operationId: backup.run
      summary: "VERIFY 1.2 RC3, PASO 2B: dispara un respaldo cifrado de VERIFY_RUNTIME_DIR (incluida una copia en caliente de la base SQLite vía better-sqlite3 db.backup()) a Cloudflare R2. Sin token: ningún alcance emitible cubre esto, el mismo razonamiento que maintenance.reconcileRun — un token que pudiera disparar o leer el respaldo de toda la instalación es una credencial que vale la pena robar."
      tags:
        - backup
      security:
        - sessionCookie: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - kind
                properties:
                  kind:
                    type: string
                    enum:
                      - OK
                      - EXISTS
                  status:
                    type:
                      - object
                      - "null"
                    additionalProperties: false
                    properties:
                      date:
                        type: string
                        format: date-time
                      bytes:
                        type: integer
                        minimum: 0
                      sha256:
                        type: string
                      status:
                        type: string
                        enum:
                          - OK
                          - FAIL
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_SESSION_REQUIRED
              cause: "hay identidad pero no es una sesión (sería un token): un token nunca sirve para esta operación"
            - code: INSTANCE_OWNER_REQUIRED
              cause: "hay sesión pero no es el Owner de la instancia"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: BACKUP_BUSY
              cause: "ya hay un respaldo en curso en este proceso o sostenido por otro vía el fichero de arrendamiento (lease) · medido en backup.ts:acquireBackupLease()"
        502:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: R2_HTTP_
              cause: "R2 respondió con un código HTTP distinto de 2xx/404 a alguna de las llamadas firmadas (HEAD/PUT/GET/DELETE); el código completo lleva el estado HTTP real de R2 como sufijo, p. ej. R2_HTTP_403 o R2_HTTP_500 · medido en backup.ts:r2Fetch()"
            - code: R2_CHECKSUM_MISMATCH
              cause: "el objeto .sha256 ya presente en R2 no coincide con el cuerpo descargado del objeto principal · medido en backup.ts:runCloudBackup()"
            - code: BACKUP_OR_NETWORK_ERROR
              cause: "cualquier otro fallo durante el empaquetado, cifrado o subida (incluida la copia en caliente de SQLite) que no sea uno de los anteriores · medido en backup.ts:runCloudBackup()"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: BACKUP_CLOUD_ONLY
              cause: "point 4: este respaldo sólo existe en el perfil de nube (VERIFY_DEPLOY=cloud); en local se rechaza aunque las 5 variables estén presentes · medido en backup.ts:runCloudBackup()"
            - code: BACKUP_KEY_MISSING
              cause: "VERIFY_BACKUP_KEY no está presente o no tiene la forma de 32 bytes exigida · medido en backup.ts:backupKey()"
            - code: BACKUP_NOT_CONFIGURED
              cause: "falta alguna de R2_ACCOUNT_ID / R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY / R2_BUCKET · medido en backup.ts:r2Config()"
      x-verify-permission: instance owner
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
  /api/v1/system/backup/status:
    get:
      operationId: backup.status
      summary: "VERIFY 1.2 RC3, PASO 2B: el estado del último respaldo (ok/edad en horas), sin secreto alguno — ni VERIFY_BACKUP_KEY ni las credenciales de R2 aparecen jamás en esta respuesta."
      tags:
        - backup
      security:
        - sessionCookie: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - configured
                  - key_configured
                  - status
                  - ok
                  - age_hours
                properties:
                  configured:
                    type: boolean
                  key_configured:
                    type: boolean
                  status:
                    type:
                      - object
                      - "null"
                    additionalProperties: false
                    properties:
                      date:
                        type: string
                        format: date-time
                      bytes:
                        type: integer
                        minimum: 0
                      sha256:
                        type: string
                      status:
                        type: string
                        enum:
                          - OK
                          - FAIL
                      reason:
                        type: string
                      lastSuccessAt:
                        type: string
                        format: date-time
                  ok:
                    type: boolean
                  age_hours:
                    type: number
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_SESSION_REQUIRED
              cause: "hay identidad pero no es una sesión (sería un token): un token nunca sirve para esta operación"
            - code: INSTANCE_OWNER_REQUIRED
              cause: "hay sesión pero no es el Owner de la instancia"
      x-verify-permission: instance owner
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
  "/api/v1/spaces/{spaceId}/invitations/bootstrap":
    post:
      operationId: members.bootstrapOwner
      summary: "Invitación de arranque creada"
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
              properties:
                email:
                  type: string
                  maxLength: 254
                  pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - role
                  - link
                  - link_es
                  - expires_in_hours
                  - delivery
                  - shown_once
                  - notice
                properties:
                  id:
                    type: string
                  role:
                    type: string
                    enum:
                      - OWNER
                  link:
                    type: string
                  link_es:
                    type: string
                  expires_in_hours:
                    type: integer
                    enum:
                      - 24
                  delivery:
                    type: string
                    enum:
                      - OWNER_MANUAL
                  shown_once:
                    type: boolean
                    enum:
                      - true
                  notice:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_SESSION_REQUIRED
              cause: "no hay sesión de administrador de instancia; un token no sirve"
            - code: SPACE_ALREADY_HAS_MEMBERS
              cause: "el espacio tiene al menos una membresía viva: este camino sólo existe para el espacio que quedaría inalcanzable"
            - code: EMAIL_INVALID
              cause: el correo no valida
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "SÓLO administrador de INSTANCIA y SÓLO por sesión. Un token de API no sirve. No concede lectura de nada dentro del espacio"
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
  "/api/v1/spaces/{spaceId}/invitations":
    post:
      operationId: members.invite
      summary: "Invitación creada"
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - email
                - role
              properties:
                email:
                  type: string
                  maxLength: 254
                  pattern: "^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$"
                role:
                  type: string
                  maxLength: 16
                  enum:
                    - OWNER
                    - OPERATOR
                    - VIEWER
                lang:
                  type: string
                  enum:
                    - en
                    - es
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - role
                  - link
                  - link_es
                  - expires_in_hours
                  - delivery
                  - email_verified
                  - email_sent
                  - notice
                  - shown_once
                properties:
                  id:
                    type: string
                  role:
                    type: string
                    enum:
                      - OWNER
                      - OPERATOR
                      - VIEWER
                  link:
                    type: string
                  link_es:
                    type: string
                  expires_in_hours:
                    type: integer
                    enum:
                      - 24
                  delivery:
                    type: string
                    enum:
                      - OWNER_MANUAL
                  email_verified:
                    type: boolean
                    enum:
                      - false
                  email_sent:
                    type: boolean
                    description: "Owner order, 2026-10-17 (RC7), pieza 3: true only when RESEND_API_KEY was configured AND the send succeeded."
                  notice:
                    type: string
                  shown_once:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: EMAIL_INVALID
              cause: el correo no valida
            - code: ROLE_UNKNOWN
              cause: "el rol pedido para la invitación no es OWNER, OPERATOR ni VIEWER · medido en routes/members.ts:107"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/members":
    get:
      operationId: members.list
      summary: Miembros del espacio
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - members
                  - invitations
                  - invitations_scope
                  - delivery_note
                properties:
                  members:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - role
                        - created_at
                        - display_name
                        - email_display
                        - status
                        - email_verified
                      properties:
                        id:
                          type: string
                        role:
                          type: string
                          enum:
                            - OWNER
                            - OPERATOR
                            - VIEWER
                        created_at:
                          type: string
                          format: date-time
                        display_name:
                          type: string
                        email_display:
                          type: string
                        email_verified:
                          type: boolean
                          description: "Owner order, 2026-10-17 (RC7), pieza 3: derived from users.email_verified_at (migration 7)."
                        status:
                          type: string
                          enum:
                            - ACTIVE
                            - DISABLED
                  invitations:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - email_display
                        - role
                        - created_at
                        - expires_at
                        - used_at
                        - revoked_at
                        - delivery
                      properties:
                        id:
                          type: string
                        email_display:
                          type: string
                        role:
                          type: string
                          enum:
                            - OWNER
                            - OPERATOR
                            - VIEWER
                        created_at:
                          type: string
                          format: date-time
                        expires_at:
                          type: string
                          format: date-time
                        used_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        revoked_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        delivery:
                          type: string
                  invitations_scope:
                    type: object
                    additionalProperties: false
                    required:
                      - class
                      - shown
                      - view_limit
                      - more_exist
                      - complete
                      - note
                    properties:
                      class:
                        type: string
                        enum:
                          - BOUNDED_SUBSET
                      shown:
                        type: integer
                        minimum: 0
                      view_limit:
                        type: integer
                        enum:
                          - 50
                      more_exist:
                        type: boolean
                      complete:
                        type: boolean
                      note:
                        type: string
                  delivery_note:
                    type: string
                    description: "H-016 residual (Codex RC4 audit), closed in RC5: a stable copy key the client translates (content/r2/copy/ui.{es,en}.json), not literal prose — the server does not choose a language. Currently always 'auth.deliveryNoteLocal'."
                description: "RC2 §4 #2 (Codex 2026-10-10), closed in RC3 by Dirección técnica's own choice (no clients exist yet to break): the catalog's success_kind was 'page', but the real handler has never produced a cursor-paginated listing — it is a deliberately bounded view, by design (see the handler's own comment: the most recent entries as context, not an archive, with its own 'more_exist' flag). Retrofitting real cursor pagination onto a collection that is small by design would be invented complexity this operation does not need; the catalog now declares what it has always really been."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-pagination:
        filters:
          - role
          - state
        order: "(created_at DESC, membership_id DESC)"
  "/api/v1/spaces/{spaceId}/invitations/{invitationId}/resend":
    post:
      operationId: members.inviteResend
      summary: "Owner order, 2026-10-17 (RC7), pieza 3: solo para una invitación no canjeada y no revocada. Issues a new invitation row (new token) and revokes the old one in the same transaction."
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: invitationId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: []
              properties:
                lang:
                  type: string
                  enum:
                    - en
                    - es
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - role
                  - link
                  - link_es
                  - expires_in_hours
                  - delivery
                  - email_sent
                  - shown_once
                properties:
                  id:
                    type: string
                  role:
                    type: string
                    enum:
                      - OWNER
                      - OPERATOR
                      - VIEWER
                  link:
                    type: string
                  link_es:
                    type: string
                  expires_in_hours:
                    type: integer
                    enum:
                      - 24
                  delivery:
                    type: string
                    enum:
                      - OWNER_MANUAL
                  email_sent:
                    type: boolean
                  shown_once:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: BODY_MUST_BE_OBJECT
              cause: "jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano."
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: la real, literal code onlyKeys() throws for any field the body declares that this operation does not accept."
            - code: INVITATION_NOT_RESENDABLE
              cause: "RC7 pieza 3: la invitación ya fue canjeada o revocada · medido en routes/members.ts"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
            - code: INVITATION_NOT_AVAILABLE
              cause: "RC7 pieza 3: la invitación no existe en este espacio · medido en routes/members.ts"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/members/{memberId}/password-reset-link":
    post:
      operationId: members.passwordResetLink
      summary: "Owner order, 2026-10-17 (RC7), pieza 4: el respaldo sin correo del Owner — mismo mecanismo que el autoservicio, nunca para una fila de Owner, nunca enviado por email."
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: memberId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - link
                  - link_es
                  - expires_in_minutes
                  - shown_once
                properties:
                  link:
                    type: string
                  link_es:
                    type: string
                  expires_in_minutes:
                    type: integer
                    enum:
                      - 60
                  shown_once:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: OWNER_PASSWORD_NOT_CHANGEABLE_HERE
              cause: "RC7 piezas 2/4/5: la contraseña del Owner nunca se cambia por esta vía · medido en routes/password_reset.ts"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
            - code: MEMBER_NOT_AVAILABLE
              cause: "RC7 pieza 4: la membresía no existe en este espacio · medido en routes/password_reset.ts"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/members/{memberId}/revoke":
    post:
      operationId: members.revoke
      summary: "Membresía revocada"
      tags:
        - members
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: memberId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - revoked
                properties:
                  id:
                    type: string
                  revoked:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: LAST_OWNER_CANNOT_BE_REVOKED
              cause: "es el último OWNER vivo del espacio: revocarlo lo dejaría sin nadie que administre · medido en routes/members.ts:145"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: MEMBER_NOT_AVAILABLE
              cause: "membresía no visible para esta identidad, incluida la de otro espacio · medido en routes/members.ts:142"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "OWNER · members.manage"
      x-verify-permission-capability: members.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/projects/{projectId}/archive":
    post:
      operationId: projects.archive
      summary: Proyecto archivado
      tags:
        - projects
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - archived
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  archived:
                    type: boolean
                    const: true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: ALREADY_ARCHIVED
              cause: "el proyecto ya está archivado · medido en routes/projects.ts:99"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · projects.archive"
      x-verify-permission-capability: projects.archive
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/projects":
    post:
      operationId: projects.create
      summary: Proyecto creado
      tags:
        - projects
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 120
                description:
                  type: string
                  maxLength: 2000
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - name
                  - description
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  name:
                    type: string
                    maxLength: 120
                  description:
                    type:
                      - string
                      - "null"
                    maxLength: 2000
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER u OPERATOR · projects.create"
      x-verify-permission-capability: projects.create
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: projects.list
      summary: Proyectos del espacio
      tags:
        - projects
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - description
                        - created_at
                        - archived_at
                      properties:
                        id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        name:
                          type: string
                          maxLength: 120
                        description:
                          type: string
                          maxLength: 2000
                        created_at:
                          type: string
                          format: date-time
                        archived_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
      x-verify-pagination:
        filters:
          - state
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/projects/{projectId}":
    get:
      operationId: projects.get
      summary: Proyecto
      tags:
        - projects
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - project
                  - counts
                  - recent_assessments
                properties:
                  project:
                    type: object
                    additionalProperties: false
                    required:
                      - id
                      - space_id
                      - name
                      - description
                      - created_by
                      - created_at
                      - archived_at
                    properties:
                      id:
                        type: string
                        pattern: "^[A-Za-z0-9_-]{1,128}$"
                      space_id:
                        type: string
                        pattern: "^[A-Za-z0-9_-]{1,128}$"
                      name:
                        type: string
                        maxLength: 120
                      description:
                        type: string
                        maxLength: 2000
                      created_by:
                        type: string
                        pattern: "^[A-Za-z0-9_-]{1,128}$"
                      created_at:
                        type: string
                        format: date-time
                      archived_at:
                        type:
                          - string
                          - "null"
                        format: date-time
                  counts:
                    type: object
                    additionalProperties: false
                    required:
                      - artifacts
                      - references
                      - evidence
                      - assessments
                    properties:
                      artifacts:
                        type: integer
                        minimum: 0
                      references:
                        type: integer
                        minimum: 0
                      evidence:
                        type: integer
                        minimum: 0
                      assessments:
                        type: integer
                        minimum: 0
                  recent_assessments:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - mode
                        - job_status
                        - comparison_status
                        - security_assessment_status
                        - created_at
                      properties:
                        id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        mode:
                          type: string
                          enum:
                            - identity
                            - bundle
                        job_status:
                          type: string
                          enum:
                            - DRAFT
                            - VALIDATING
                            - NEEDS_INPUT
                            - QUEUED
                            - RUNNING
                            - COMPLETED
                            - ERROR
                            - INTERRUPTED
                            - CANCELLED
                        comparison_status:
                          type: string
                        security_assessment_status:
                          type:
                            - string
                            - "null"
                        created_at:
                          type: string
                          format: date-time
                    maxItems: 5
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
  "/api/v1/spaces/{spaceId}/references/{referenceId}/approve":
    post:
      operationId: references.approve
      summary: Referencia aprobada
      tags:
        - references
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: referenceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - state
                  - trust_level
                properties:
                  id:
                    type: string
                  state:
                    type: string
                    enum:
                      - APPROVED
                  trust_level:
                    type: string
                    enum:
                      - ORG_APPROVED
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: REFERENCE_NOT_AVAILABLE
              cause: "referencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:66"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: REFERENCE_NOT_DRAFT
              cause: "sólo se aprueba un borrador · medido en RC01 routes/references.ts:119"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · references.approve"
      x-verify-permission-capability: references.approve
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/projects/{projectId}/references":
    post:
      operationId: references.create
      summary: "Referencia PROPUESTA, todavía no aprobada"
      tags:
        - references
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - label
                - expected_sha256
                - origin_kind
                - origin_text
              properties:
                label:
                  type: string
                  maxLength: 120
                expected_sha256:
                  type: string
                  maxLength: 64
                  description: "Lower-cased by the handler before the HEX64 check, so an upper-case 64-hex-digit value is also accepted."
                origin_kind:
                  type: string
                  maxLength: 40
                  enum:
                    - MANUAL_EXPECTED_SHA256
                    - IMPORTED_MANIFEST
                    - PRIOR_ASSESSMENT
                    - GITHUB_ARTIFACT_DIGEST
                origin_text:
                  type: string
                  maxLength: 1000
                supersedes_id:
                  type:
                    - string
                    - "null"
                derived_from_artifact_id:
                  type:
                    - string
                    - "null"
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - state
                  - trust_level
                  - warning
                properties:
                  id:
                    type: string
                  state:
                    type: string
                    enum:
                      - DRAFT
                  trust_level:
                    type: string
                    enum:
                      - USER_DECLARED
                  warning:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: DERIVED_DIGEST_DOES_NOT_MATCH_ARTIFACT
              cause: "el sha256 declarado no coincide con el medido del artefacto de origen · medido en routes/references.ts:74"
            - code: EXPECTED_SHA256_INVALID
              cause: "el sha256 declarado no tiene forma hexadecimal de 64 caracteres · medido en routes/references.ts:63"
            - code: ORIGIN_KIND_UNKNOWN
              cause: "el origen declarado para la referencia no está en la lista admitida · medido en routes/references.ts:65"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: ARTIFACT_NOT_AVAILABLE
              cause: "artefacto no visible para esta identidad, incluido el de otro espacio · medido en routes/assessments.ts:42"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: REFERENCE_NOT_AVAILABLE
              cause: "referencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:66"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER u OPERATOR · references.propose"
      x-verify-permission-capability: references.propose
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: references.propose
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: references.list
      summary: Referencias del proyecto
      tags:
        - references
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - label
                        - immutable_version
                        - expected_sha256
                        - origin_kind
                        - origin_text
                        - trust_level
                        - state
                        - proposed_at
                        - approved_at
                        - supersedes_id
                        - proposed_by_name
                        - approved_by_name
                      properties:
                        id:
                          type: string
                        label:
                          type: string
                        immutable_version:
                          type: integer
                          minimum: 1
                        expected_sha256:
                          type: string
                          pattern: "^[a-f0-9]{64}$"
                        origin_kind:
                          type: string
                          enum:
                            - MANUAL_EXPECTED_SHA256
                            - IMPORTED_MANIFEST
                            - PRIOR_ASSESSMENT
                            - GITHUB_ARTIFACT_DIGEST
                        origin_text:
                          type: string
                        trust_level:
                          type: string
                          enum:
                            - USER_DECLARED
                            - ORG_APPROVED
                            - PROVIDER_METADATA
                        state:
                          type: string
                          enum:
                            - DRAFT
                            - APPROVED
                            - SUPERSEDED
                            - REVOKED
                        proposed_at:
                          type: string
                          format: date-time
                        approved_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        supersedes_id:
                          type:
                            - string
                            - "null"
                        proposed_by_name:
                          type: string
                        approved_by_name:
                          type:
                            - string
                            - "null"
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
            - code: LIMIT_OUT_OF_RANGE
              cause: "el límite de página pedido no es un entero dentro del máximo declarado · medido en http.ts:199"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
      x-verify-pagination:
        filters:
          - state
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/references/{referenceId}/revoke":
    post:
      operationId: references.revoke
      summary: Referencia revocada
      tags:
        - references
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: referenceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - state
                properties:
                  id:
                    type: string
                  state:
                    type: string
                    enum:
                      - REVOKED
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: REFERENCE_NOT_AVAILABLE
              cause: "referencia no visible para esta identidad, incluida la de otro espacio · medido en routes/assessments.ts:66"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ALREADY_REVOKED
              cause: "ya estaba revocada · medido en RC01 routes/references.ts:142"
      x-verify-permission: "OWNER · references.approve"
      x-verify-permission-capability: references.approve
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  "/api/v1/spaces/{spaceId}/requirements/{requirementId}/approve":
    post:
      operationId: requirements.approve
      summary: Perfil aprobado
      tags:
        - requirements
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: requirementId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - state
                properties:
                  id:
                    type: string
                  state:
                    type: string
                    enum:
                      - APPROVED
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: REQUIREMENTS_NOT_AVAILABLE
              cause: "requisito no visible para esta identidad, incluido el de otro espacio · medido en routes/assessments.ts:48"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ALREADY_APPROVED
              cause: "ya estaba aprobada · medido en RC01 routes/references.ts:221"
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · requirements.approve"
      x-verify-permission-capability: requirements.approve
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  "/api/v1/spaces/{spaceId}/projects/{projectId}/requirements":
    post:
      operationId: requirements.create
      summary: Perfil propuesto
      tags:
        - requirements
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - template_id
              properties:
                template_id:
                  type: string
                  maxLength: 80
                template_version:
                  type: integer
                  minimum: 1
                  description: "Read via Number(body.template_version ?? 1) rather than str() — defaults to 1 when absent."
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - template_id
                  - version
                  - state
                  - supported_profile
                  - answers
                  - limits
                properties:
                  id:
                    type: string
                  template_id:
                    type: string
                  version:
                    type: integer
                    minimum: 1
                  state:
                    type: string
                    enum:
                      - APPROVED
                  supported_profile:
                    type: string
                  answers:
                    type: string
                  limits:
                    type: array
                    items:
                      type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: TEMPLATE_ADAPTER_NOT_BUILT
              cause: "la plantilla existe y su adaptador todavía no está implementado · medido en routes/references.ts:190"
            - code: TEMPLATE_UNKNOWN
              cause: "la plantilla pedida no está en el catálogo de plantillas · medido en routes/references.ts:186"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "OWNER · requirements.approve"
      x-verify-permission-capability: requirements.approve
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: requirements.list
      summary: Perfiles del proyecto
      tags:
        - requirements
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: projectId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - templates
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - template_id
                        - version
                        - supported_profile
                        - canonical_sha256
                        - state
                        - created_at
                        - approved_at
                      properties:
                        id:
                          type: string
                        template_id:
                          type: string
                        version:
                          type: integer
                          minimum: 1
                        supported_profile:
                          type: string
                        canonical_sha256:
                          type: string
                          pattern: "^[a-f0-9]{64}$"
                        state:
                          type: string
                          enum:
                            - DRAFT
                            - APPROVED
                            - SUPERSEDED
                            - REVOKED
                        created_at:
                          type: string
                          format: date-time
                        approved_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                  templates:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - version
                        - mode
                        - supported_profile
                        - availability
                        - answers
                        - limits
                      properties:
                        id:
                          type: string
                        version:
                          type: integer
                          minimum: 1
                        mode:
                          type: string
                          enum:
                            - identity
                            - bundle
                        supported_profile:
                          type: string
                        availability:
                          type: string
                          enum:
                            - AVAILABLE
                            - ADAPTER_NOT_BUILT
                        answers:
                          type: string
                        limits:
                          type: array
                          items:
                            type: string
                description: "H-005 residual note: this operation's success_kind is catalogued as 'json' and this IS a plain json response — but note its unusual shape: a catalog-style endpoint returning both the project's own requirement sets AND the static list of available templates in one body."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_PROJECT_DENIED
              cause: "el token está acotado a otro proyecto · medido en authz.ts:99"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · projects.read"
      x-verify-permission-capability: projects.read
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: projects.read
      x-verify-pagination:
        filters:
          - state
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/retention":
    get:
      operationId: retention.get
      summary: "Política vigente"
      tags:
        - retention
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - space_id
                  - version
                  - rules
                  - configured
                  - updated_at
                  - updated_by
                properties:
                  space_id:
                    type: string
                  version:
                    type: integer
                    minimum: 0
                  rules:
                    type: array
                    items:
                      type: object
                    description: "retention.ts's own write-side check: every rule must be a plain, non-array object."
                  configured:
                    type: boolean
                  updated_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  updated_by:
                    type:
                      - string
                      - "null"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · retention.read"
      x-verify-permission-capability: retention.read
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
    post:
      operationId: retention.set
      summary: "Política aceptada y versionada"
      tags:
        - retention
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - rules
                - accepted_by_owner
              properties:
                rules:
                  type: array
                  items:
                    type: object
                accepted_by_owner:
                  type: boolean
                  enum:
                    - true
                  description: "Must be exactly true. RC2 §4 #6 (Codex 2026-10-10), closed in RC3: the real handler answers 422 RETENTION_NOT_ACCEPTED for anything else (false or absent) — now declared as its own error in this operation's errors[] (and in the generated OpenAPI document's 422 response), not left implicit under the generic 400 VALIDATION_FAILED."
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - space_id
                  - version
                  - rules
                  - configured
                  - updated_at
                  - updated_by
                properties:
                  space_id:
                    type: string
                  version:
                    type: integer
                    minimum: 0
                  rules:
                    type: array
                    items:
                      type: object
                    description: "retention.ts's own write-side check: every rule must be a plain, non-array object."
                  configured:
                    type: boolean
                  updated_at:
                    type:
                      - string
                      - "null"
                    format: date-time
                  updated_by:
                    type:
                      - string
                      - "null"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: IDEMPOTENCY_KEY_REPEATED
              cause: "la cabecera Idempotency-Key llegó repetida en la misma petición · medido en idempotency.ts:352"
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
        422:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: RETENTION_NOT_ACCEPTED
              cause: "la política llegó y no es interpretable · medido en RC01 routes/retention.ts:97"
      x-verify-permission: "OWNER · retention.manage"
      x-verify-permission-capability: retention.manage
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
  /api/v1/spaces:
    post:
      operationId: spaces.create
      summary: Espacio creado
      tags:
        - spaces
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
              properties:
                name:
                  type: string
                  maxLength: 120
                join_as_owner:
                  type: boolean
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - name
                  - role
                  - creator_is_a_member
                  - note
                properties:
                  id:
                    type: string
                    pattern: "^[A-Za-z0-9_-]{1,128}$"
                  name:
                    type: string
                    maxLength: 120
                  role:
                    type:
                      - string
                      - "null"
                    enum:
                      - OWNER
                      - null
                  creator_is_a_member:
                    type: boolean
                  note:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_REQUIRED
              cause: "quien pide no es el instance owner de esta instalación"
            - code: TOKEN_CANNOT_ADMINISTER_INSTANCE
              cause: "un token nunca administra la instancia, sólo una sesión de su Owner · medido en routes/spaces.ts:30"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: instance owner
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
      x-verify-idempotency-scope: "(instalación, operationId, clave)"
    get:
      operationId: spaces.list
      summary: "Espacios con membresía vigente"
      tags:
        - spaces
      security:
        - sessionCookie: []
        - bearerToken: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: page
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - page
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - kind
                        - status
                        - role
                      properties:
                        id:
                          type: string
                          pattern: "^[A-Za-z0-9_-]{1,128}$"
                        name:
                          type: string
                        kind:
                          type: string
                          enum:
                            - CLIENT
                            - DEMO
                        status:
                          type: string
                          enum:
                            - ACTIVE
                            - ARCHIVED
                        role:
                          type: string
                          enum:
                            - OWNER
                            - OPERATOR
                            - VIEWER
                  page:
                    $ref: "#/components/schemas/PageInfo"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
      x-verify-permission: identidad propia
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN_NO_SCOPE
      x-verify-pagination:
        filters: []
        order: "(created_at DESC, id DESC)"
  /api/v1/healthz:
    get:
      operationId: system.health
      summary: El proceso responde. No dice si puede aceptar trabajo
      tags:
        - system
      security: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Health"
      x-verify-permission: "público"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  /api/v1/system/readiness:
    get:
      operationId: system.readiness
      summary: "Detalle sólo para el operador"
      tags:
        - system
      security:
        - sessionCookie: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OperatorReadiness"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_REQUIRED
              cause: "quien pide no es el instance owner de esta instalación"
            - code: INSTANCE_OWNER_SESSION_REQUIRED
              cause: "no hay sesión de administrador de instancia; un token no sirve"
        503:
          description: "Not ready. The SAME body as the 200, for the instance administrator, with ready false and what is blocking."
          x-verify-not-an-error-envelope: true
          content:
            application/json:
              schema:
                type: object
      x-verify-permission: "SÓLO administrador de INSTANCIA. Un OWNER de espacio NO alcanza este endpoint: instancia y espacio son roles distintos"
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: INSTANCE_OWNER_SESSION
  /api/v1/readyz:
    get:
      operationId: system.readyz
      summary: "Disponibilidad mínima para aceptar trabajos"
      tags:
        - system
      security: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Readiness"
        503:
          description: "Not ready. The SAME body as the 200, with ready false and what is blocking. It is not an error envelope."
          x-verify-not-an-error-envelope: true
          content:
            application/json:
              schema:
                type: object
      x-verify-permission: "público"
      x-verify-identity: []
      x-verify-identity-model: ANONYMOUS
  "/api/v1/spaces/{spaceId}/tokens":
    post:
      operationId: tokens.create
      summary: Token creado
      tags:
        - tokens
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required:
                - name
                - scopes
              properties:
                name:
                  type: string
                  maxLength: 120
                scopes:
                  type: array
                  items:
                    type: string
                  minItems: 1
                project_id:
                  type:
                    - string
                    - "null"
                expires_in_days:
                  type: integer
                  minimum: 1
                  maximum: 365
      responses:
        201:
          description: Created.
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - name
                  - scopes
                  - project_id
                  - token
                  - shown_once
                  - notice
                properties:
                  id:
                    type: string
                  name:
                    type: string
                  scopes:
                    type: array
                    items:
                      type: string
                  project_id:
                    type:
                      - string
                      - "null"
                  token:
                    type: string
                  shown_once:
                    type: boolean
                    enum:
                      - true
                  notice:
                    type: string
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: EXPIRY_OUT_OF_RANGE
              cause: "la caducidad pedida para el token está fuera del rango admitido (1 a 365 días) · medido en routes/members.ts:188"
            - code: SCOPES_REQUIRED
              cause: "se pide mintar un token sin ningún alcance · medido en routes/members.ts:184"
            - code: TOKEN_CANNOT_MINT_TOKEN
              cause: "un token no minta otro token: sólo una sesión lo hace · medido en routes/members.ts:179"
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
            - code: TOKEN_SCOPE_EXCEEDS_ROLE
              cause: "se pide mintar un token con un alcance que el rol de quien lo pide ya no tiene · medido en authz.ts:63"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: PROJECT_NOT_AVAILABLE
              cause: "proyecto no visible para esta identidad, incluido el de otro espacio · medido en routes/connections.ts:99"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: IDEMPOTENCY_MISMATCH
              cause: "la misma clave con otro cuerpo o desde otro principal: no se ejecuta nada"
            - code: IDEMPOTENCY_IN_PROGRESS
              cause: "la operación original con esa misma clave sigue en curso: se responde en el tope declarado de espera sin ejecutar nada nuevo"
            - code: IDEMPOTENT_REPLAY_OF_FAILURE
              cause: "la ejecución anterior con esa misma clave terminó en fallo: se devuelve ese fallo sin reejecutar, y nunca como 2xx ni con el estado del intento original"
        410:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: GONE
              cause: "el objeto existió y ya no sirve: invitación caducada o revocada, o el resultado al que apuntaba una clave de idempotencia ya purgado. En el segundo caso la operación NO se vuelve a ejecutar"
      x-verify-permission: "miembro · tokens.create.own"
      x-verify-permission-capability: tokens.create.own
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-idempotency-scope: "(space_id, operationId, clave)"
    get:
      operationId: tokens.list
      summary: Tokens propios del espacio
      tags:
        - tokens
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - items
                  - secret_returned
                properties:
                  items:
                    type: array
                    items:
                      type: object
                      additionalProperties: false
                      required:
                        - id
                        - name
                        - project_id
                        - scopes
                        - created_at
                        - expires_at
                        - last_used_at
                        - revoked_at
                      properties:
                        id:
                          type: string
                        name:
                          type: string
                        project_id:
                          type:
                            - string
                            - "null"
                        scopes:
                          type: array
                          items:
                            type: string
                          description: "RC2 §4 #3 (Codex 2026-10-10), closed in RC3: the handler now splits the comma-joined storage column into a real array — the same shape as tokens.create's response, for the same field."
                        created_at:
                          type: string
                          format: date-time
                        expires_at:
                          type: string
                          format: date-time
                        last_used_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                        revoked_at:
                          type:
                            - string
                            - "null"
                          format: date-time
                  secret_returned:
                    type: boolean
                    enum:
                      - false
                description: "RC2 §4 #2 (Codex 2026-10-10), closed in RC3 by Dirección técnica's own choice (no clients exist yet to break): the catalog's success_kind was 'page', but the real handler has never produced a cursor-paginated listing — it is a deliberately bounded view, by design (see the handler's own comment: the most recent entries as context, not an archive, with its own 'more_exist' flag). Retrofitting real cursor pagination onto a collection that is small by design would be invented complexity this operation does not need; the catalog now declares what it has always really been."
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
            - code: CURSOR_INVALID
              cause: "el cursor no corresponde a esta operación o a estos filtros; NUNCA se interpreta como el comienzo de la lista"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
      x-verify-permission: "miembro · tokens.create.own"
      x-verify-permission-capability: tokens.create.own
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
      x-verify-pagination:
        filters:
          - state
        order: "(created_at DESC, id DESC)"
  "/api/v1/spaces/{spaceId}/tokens/{tokenId}/revoke":
    post:
      operationId: tokens.revoke
      summary: Token revocado
      tags:
        - tokens
      security:
        - sessionCookie: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: tokenId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                type: object
                additionalProperties: false
                required:
                  - id
                  - revoked
                properties:
                  id:
                    type: string
                  revoked:
                    type: boolean
                    enum:
                      - true
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: entrada malformada o campo obligatorio ausente
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "el rol de quien pide no está en la lista que esta acción admite · medido en authz.ts:52 y auth/identity.ts:118"
            - code: TOKEN_SCOPE_DENIED
              cause: "el token no fue emitido con el alcance que esta acción exige · medido en authz.ts:56"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "objeto no visible para esta identidad, incluido el de otro espacio: respuesta uniforme para no revelar existencia"
            - code: SPACE_NOT_AVAILABLE
              cause: "el espacio no existe o no es visible: respuesta uniforme"
            - code: TOKEN_NOT_AVAILABLE
              cause: "token no visible para esta identidad, incluido el de otro espacio · medido en routes/members.ts:222"
      x-verify-permission: "miembro · tokens.create.own"
      x-verify-permission-capability: tokens.create.own
      x-verify-identity:
        - session_cookie
      x-verify-identity-model: SESSION_ONLY
  /api/v1/admin/site-mode:
    get:
      operationId: siteMode.get
      summary: El estado del modo portada
      tags:
        - siteMode
      security:
        - sessionCookie: []
        - bearerToken: []
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiteModeState"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_REQUIRED
              cause: "quien pide no es el instance owner de esta instalación"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: SITE_IN_PORTADA
              cause: "modo portada activo y quien pide no es el instance owner — el gate genérico responde antes de llegar aquí, así que un no-owner nunca ve el 403 propio de esta operación en ese estado"
      x-verify-permission: instance owner
      x-verify-permission-capability: site_mode.manage
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: site_mode.manage
    put:
      operationId: siteMode.set
      summary: Enciende o apaga el modo portada
      tags:
        - siteMode
      security:
        - sessionCookie: []
        - bearerToken: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SiteModeSetRequest"
      responses:
        200:
          description: "Completed. A 200 reports transport, never a verdict."
          x-verify-success-kind: json
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SiteModeState"
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: VALIDATION_FAILED
              cause: "mode no es OPEN ni PORTADA, o el cuerpo lleva un campo no declarado"
            - code: UNKNOWN_FIELDS
              cause: "RC2 §4 #8 (Codex 2026-10-10), closed in RC3: the real, literal code onlyKeys() throws for any field the body declares that this operation does not accept — measured per operation in tests/server/h005-schema-conformance.test.mjs, not assumed to be the generic VALIDATION_FAILED below."
            - code: FIELD_NOT_STRING
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): el campo llegó, pero no es una cadena · medido en http.ts:334."
            - code: FIELD_REQUIRED
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): un campo de cadena marcado required:true falta en el cuerpo · medido en http.ts:332. El sufijo tras ':' nombra el campo y no es parte del código — mismo convenio que ROLE_DENIED:<action>."
            - code: FIELD_TOO_LONG
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena excede el máximo de caracteres declarado para ese campo · medido en http.ts:335."
            - code: FIELD_TOO_SHORT
              cause: "RC5, order §A.3 (Codex RC4 audit, API-MUESTRA.md): str(): la cadena no alcanza el mínimo de caracteres declarado para ese campo · medido en http.ts:336."
            - code: BODY_MUST_BE_OBJECT
              cause: "RC6, order §A.1 (Codex RC5 audit, H-013): jsonBody() en http.ts:318 exige que el cuerpo JSON decodificado sea un objeto plano; un array, una cadena, un número o null responden este código antes de que la operación lea ningún campo."
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: "sin sesión ni token válidos"
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_REQUIRED
              cause: "quien pide no es el instance owner de esta instalación"
        503:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: SITE_IN_PORTADA
              cause: "modo portada activo y quien pide no es el instance owner — el gate genérico responde antes de llegar aquí, así que un no-owner nunca ve el 403 propio de esta operación en ese estado"
      x-verify-permission: instance owner
      x-verify-permission-capability: site_mode.manage
      x-verify-identity:
        - session_cookie
        - api_token
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: site_mode.manage
  "/api/v1/spaces/{spaceId}/uploads/{uploadId}/bytes":
    put:
      operationId: local.put_api_v1_spaces_spaceId_uploads_uploadId_bytes
      summary: "Local addition, not a contract operation. Serves artifacts.initUpload / artifacts.complete."
      tags:
        - local
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: spaceId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
        - name: uploadId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      requestBody:
        required: true
        content:
          application/octet-stream:
            schema:
              type: string
              format: binary
      responses:
        200:
          description: "huella medida por el servidor y tamaño medido. A 2xx reports transport, never a verdict."
          content:
            application/json:
              schema:
                type: object
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: NOT_AUTHENTICATED
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: ROLE_DENIED
              cause: "ROLE_DENIED o TOKEN_SCOPE_DENIED (el rol o el alcance del token no admite artifacts.upload) · y TOKEN_PROJECT_DENIED cuando el token está ligado a otro proyecto, por el invariante de B-01 · medido en authz.ts, llamado desde routes/artifacts_staged.ts:191"
            - code: TOKEN_SCOPE_DENIED
              cause: "ROLE_DENIED o TOKEN_SCOPE_DENIED (el rol o el alcance del token no admite artifacts.upload) · y TOKEN_PROJECT_DENIED cuando el token está ligado a otro proyecto, por el invariante de B-01 · medido en authz.ts, llamado desde routes/artifacts_staged.ts:191"
            - code: TOKEN_PROJECT_DENIED
              cause: "ROLE_DENIED o TOKEN_SCOPE_DENIED (el rol o el alcance del token no admite artifacts.upload) · y TOKEN_PROJECT_DENIED cuando el token está ligado a otro proyecto, por el invariante de B-01 · medido en authz.ts, llamado desde routes/artifacts_staged.ts:191"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "NOT_FOUND · la reserva no existe o no es de este espacio. El 404 es uniforme aquí igual que en las 69"
        409:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: UPLOAD_NOT_OPEN
              cause: "UPLOAD_NOT_OPEN · la reserva ya no admite bytes"
        413:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: FILE_EXCEEDS_LIMIT
              cause: "FILE_EXCEEDS_LIMIT · el archivo excede el límite o llega truncado"
      x-verify-permission: "la del artefacto que transfiere: artifacts.upload"
      x-verify-identity: SESSION_OR_TOKEN
      x-verify-identity-model: SESSION_OR_TOKEN
      x-verify-required-token-scope: artifacts.upload
      x-verify-scope: "espacio + proyecto del artefacto"
      x-verify-product-operation: false
      x-verify-not-in-contract: "es la fase 2 de una subida de tres fases. Declararla operación inflaría el número contra el que se mide todo el mandato; servirla sin declararla dejaría superficie oculta"
      x-verify-auxiliary-id: AUX-01
  "/api/v1/system/reconcile/{runId}/findings":
    get:
      operationId: local.get_api_v1_system_reconcile_runId_findings
      summary: "Local addition, not a contract operation. Serves maintenance.reconcileList / maintenance.reconcileRun."
      tags:
        - local
      security:
        - sessionCookie: []
        - bearerToken: []
      parameters:
        - name: runId
          in: path
          required: true
          schema:
            type: string
            pattern: "^[A-Za-z0-9_-]{1,128}$"
          description: Opaque server-generated identifier. Membership is re-checked for every request.
      responses:
        200:
          description: "los hallazgos del barrido, paginados. A 2xx reports transport, never a verdict."
          content:
            application/json:
              schema:
                type: object
        400:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: CURSOR_INVALID
              cause: "CURSOR_INVALID cuando el cursor no es de este barrido · medido en http.ts:184 (badRequest, 400, no 409)"
        401:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_AUTHENTICATED
              cause: NOT_AUTHENTICATED
        403:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: INSTANCE_OWNER_SESSION_REQUIRED
              cause: "INSTANCE_OWNER_SESSION_REQUIRED o INSTANCE_OWNER_REQUIRED, según cuál de las dos mitades falló"
            - code: INSTANCE_OWNER_REQUIRED
              cause: "INSTANCE_OWNER_SESSION_REQUIRED o INSTANCE_OWNER_REQUIRED, según cuál de las dos mitades falló"
        404:
          $ref: "#/components/responses/Error"
          x-error-codes:
            - code: NOT_FOUND
              cause: "NOT_FOUND · el barrido no existe. 404 uniforme"
      x-verify-permission: "instanceAdministrator(req) · la misma del barrido que detalla. Un API token NO la alcanza"
      x-verify-identity: INSTANCE_OWNER_SESSION
      x-verify-identity-model: INSTANCE_OWNER_SESSION
      x-verify-scope: "instalación · no es de un espacio"
      x-verify-product-operation: false
      x-verify-not-in-contract: "detalle de un barrido ya listado por una de las 69. No añade capacidad: desglosa la de maintenance.reconcileList"
      x-verify-auxiliary-id: AUX-02
x-verify-status: IMPLEMENTED_AND_LOCALLY_TESTED
x-verify-not-claimed:
  - Not a public service. This document describes a local installation.
  - "Implemented and locally tested is not certified, not audited and not deployed."
