# API de defectos

> Crear, listar, actualizar y eliminar defectos por proyecto en la API para integradores.

Los defectos son registros de bugs por proyecto con numeración secuencial (`defectNumber`, mostrado como `D-<n>` en la interfaz). El reporter se deriva del actor autenticado al crear y no puede enviarse en el cuerpo de la petición.

## Crear

`POST /api/v1/projects/{projectId}/defects`

`{projectId}` es el **código** del proyecto (por ejemplo `ACME`).

Requiere el permiso de escritura de defectos (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`.

### Cuerpo de la petición

| Campo               | Obligatorio | Notas                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| ------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `title`             | sí          | Cadena no vacía                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `description`       | no          | Texto Markdown, o `descriptionDocument` (ver [Campos de texto enriquecido](https://docs.probara.net/es/developers/api-reference/#campos-de-texto-enriquecido))                                                                                                                                                                                                                                                                                                     |
| `assigneeUlid`      | no          | ULID de miembro de la organización                                                                                                                                                                                                                                                                                                                                                                                                         |
| `tags`              | no          | Arreglo de cadenas                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `milestoneUlid`     | no          | Hito del mismo proyecto                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `customFieldValues` | no          | Arreglo de `{ fieldUlid, value }` para campos de defecto visibles. Los campos de sistema **Severity** y **Priority** son obligatorios con opciones por defecto de la organización; si omites valores que siguen siendo el predeterminado, el servidor los persiste. Otras claves omitidas se satisfacen al crear si el campo tiene un valor por defecto almacenado no vacío; en caso contrario el create devuelve `422 validation_failed`. |

`reporter`, `resolvedAt` y `closedAt` se rechazan si el cliente los envía.

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"El login falla con contraseña expirada"}' \
  "https://app.probara.net/api/v1/projects/ACME/defects"
```

## Listar

`GET /api/v1/projects/{projectId}/defects`

Paginación por página (`page`, `pageSize`), los más recientes primero. La respuesta sigue el formato de los listados de tabla (`items`, `page`, `pageSize`, `total`), donde `total` cuenta todos los defectos que coinciden con los filtros activos y `q`. Cada ítem incluye los campos del defecto más `occurrenceCount`, `lastSeenAt` (agregados desde intentos vinculados) y `systemFieldValues` (solo entradas de severidad/prioridad del sistema). Sin `fields`, los ítems del listado **no** incluyen `customFieldValues`.

Pasa `fields` como lista separada por comas de ULIDs de campos personalizados de defecto visibles en el proyecto para embeber `customFieldValues` en cada ítem (valor guardado o predeterminado materializado, mismo formato que el detalle). Un campo fuera del alcance del proyecto devuelve `422 validation_failed`.

### Filtros de consulta

Los filtros se combinan con AND entre parámetros y se evalúan en el servidor. Los
parámetros con valores de conjunto `severity`, `priority` y `assigneeUlid` aceptan un
conjunto de ULID separados por comas y se combinan con OR **dentro** de cada
parámetro; un valor único se acepta como un conjunto de un elemento, por lo que las
URL anteriores con un solo valor siguen funcionando.

| Parámetro      | Notas                                                                                                                                                                                             |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`       | Conjunto separado por comas, p. ej. `open,in_progress`                                                                                                                                            |
| `severity`     | Conjunto separado por comas de ULID de opción del campo de sistema `defect_severity` (OR dentro; coincide con defectos que tengan cualquiera)                                                     |
| `priority`     | Conjunto separado por comas de ULID de opción del campo de sistema `defect_priority` (OR dentro)                                                                                                  |
| `assigneeUlid` | Conjunto separado por comas de ULID de miembros de la organización (OR dentro); un ULID que no resuelve a ningún miembro se ignora, y el filtro no coincide con nada solo cuando ninguno resuelve |
| `q`            | Subcadena del título, o número de defecto con `D-<n>` o entero                                                                                                                                    |
| `aging`        | Antigüedad de defectos abiertos: `d0_7`, `d7_14`, `d14_30`, `d30plus` (por `created_at`)                                                                                                          |
| `fields`       | ULIDs de campos personalizados separados por comas para embeber por ítem (`customFieldValues`)                                                                                                    |
| `cf`           | Filtro repetible por campo personalizado: `<fieldUlid>:<valor[,valor…]>` (ver abajo)                                                                                                              |

Valores de `status` inválidos devuelven `422 validation_failed`.

#### Filtros de campos personalizados (`cf`)

Repite el parámetro `cf` una vez por campo. Varias entradas `cf` se combinan con AND. Dentro de un campo, los valores separados por comas se combinan con OR.

Tipos admitidos: `select`, `select_multi`, `checkbox` y `user_picker`. Otros tipos (incluido `paragraph`) devuelven `422 validation_failed`. Los filtros coinciden solo con **filas de valor guardadas** — un predeterminado materializado en lectura no satisface un filtro por opción hasta persistirse.

| Token de valor   | Significado                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------ |
| ULID de opción   | Defecto guardado con esa opción (`select`) o que la contiene (`select_multi`)                    |
| `true` / `false` | Valor guardado de casilla                                                                        |
| ULID de miembro  | Miembro guardado en `user_picker`                                                                |
| `empty`          | Sin fila guardada para el campo (incluidos defectos que mostrarían un predeterminado en detalle) |

Ejemplo — defectos en Staging **o** Production (mismo campo, OR por comas):

```http
GET /api/v1/projects/ACME/defects?cf=01HENVFIELD:01HSTAGING,01HPROD
```

Ejemplo — sin valor de Environment **o** Staging, **y** casilla Risk aceptado:

```http
GET /api/v1/projects/ACME/defects?cf=01HENVFIELD:empty,01HSTAGING&cf=01HRISKFIELD:true
```

Ejemplo — embeber columnas Environment y Owner:

```http
GET /api/v1/projects/ACME/defects?fields=01HENVFIELD,01HOWNERFIELD
```

## Métricas del tablero

`GET /api/v1/projects/{projectId}/defects/metrics`

Respuesta única para la franja de métricas del listado y la insignia de la barra
lateral. Todas las cifras usan la organización y el proyecto activos; peticiones
fuera del inquilino devuelven `404 not_found`.

| Campo            | Definición                                                                                                                                                                           |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `totalOpen`      | Defectos en `open` o `in_progress`                                                                                                                                                   |
| `openBySeverity` | Objeto con `options` (arreglo de `{ optionUlid, systemKey, name, icon, color, count }` en orden de opción, incluyendo ceros) y `unset` (defectos abiertos sin fila de severidad)     |
| `aging`          | Población abierta por antigüedad desde `created_at`: `d0_7`, `d7_14`, `d14_30`, `d30plus`                                                                                            |
| `reopenRate`     | Defectos distintos con ≥1 evento `defect.reopened` ÷ defectos distintos resueltos alguna vez (`defect.status_changed` con `to` `resolved` o `closed`); `null` si el denominador es 0 |
| `mttrMs`         | Media de `resolved_at − created_at` sobre defectos actualmente `resolved`/`closed` con `resolved_at` no nulo; `null` si no hay ninguno                                               |
| `byMilestone`    | Población abierta por hito (`ulid`, `name`, `count`) más `unassigned`                                                                                                                |

## Defectos vinculados a una ejecución

`GET /api/v1/runs/{runUlid}/defects`

Lista defectos distintos vinculados a cualquier intento de la ejecución (join
`defect_result_links` → `test_results` de la ejecución). Cada ítem incluye los
campos públicos del defecto más `linkCountInRun` (solo vínculos en esta ejecución), `assigneeUlid` (ULID del asignado, o `null` si no hay asignado) y `systemFieldValues` de severidad/prioridad.
Devuelve `{ items: [...] }` (sin paginación). Ejecuciones desconocidas o fuera del
inquilino devuelven `404 not_found`. Funciona en ejecuciones abiertas y cerradas.

## Detalle

`GET /api/v1/defects/{defectUlid}`

Devuelve el payload completo del defecto: vocabulario, ULIDs de personas, etiquetas, hito, referencia duplicate-of, marcas de tiempo del ciclo de vida, el resumen derivado (`occurrenceCount`, `lastSeenAt`, `affectedCaseCount`) y `customFieldValues` embebidos para cada campo de defecto visible en el proyecto (valor guardado o predeterminado materializado, con enriquecimiento de opciones cuando aplica). El listado de defectos del proyecto incluye `customFieldValues` solo cuando se solicita con `fields`.

## Valores de campos personalizados

Las definiciones con alcance de defecto se gestionan en `GET/POST/PATCH/DELETE /api/v1/orgs/{orgUlid}/custom-fields` con `entity: defect`. Los valores se almacenan por par `(campo, defecto)`.

### Reemplazar todos los valores visibles

`PUT /api/v1/defects/{defectUlid}/custom-field-values`

Cuerpo: `{ "values": [ { "fieldUlid": "...", "value": ... }, ... ] }` — una entrada por campo visible. Los campos obligatorios deben estar en el conjunto de escritura. Emite `defect.updated` con `metadata.customFieldChanges` (no un evento `custom_field.*` aparte).

### Parchear un campo

`PATCH /api/v1/defects/{defectUlid}/custom-field-values/{fieldUlid}`

Cuerpo: `{ "value": ... }`. En tipos anulables, `null` borra el valor; los obligatorios rechazan vaciados con `422 validation_failed`. Devuelve el arreglo embebido completo `customFieldValues` del defecto.

## Actualizar

`PATCH /api/v1/defects/{defectUlid}`

Actualización parcial de `title`, `description`, `status`, `resolution`, `assigneeUlid`, `tags`, `milestoneUlid` y `duplicateOfUlid`. Severidad y prioridad se editan con los endpoints de valores de campo personalizado, no con este PATCH.

Las transiciones de estado siguen un mapa guiado (`open`/`in_progress` → `resolved`/`closed`; `resolved` → `closed`/`open`; `closed` → `open`). Las transiciones ilegales devuelven `422 validation_failed` con ruta de campo `status`. Los parches sin cambio de estado aplican otros campos sin emitir evento de estado.

- Pasar a `resolved` o `closed` exige `resolution`.
- `resolution = duplicate` exige `duplicateOfUlid`; las cadenas se aplanan al raíz canónico al escribir.
- Reabrir a `open` desde `resolved` o `closed` limpia `resolution`, `duplicateOfUlid` y las marcas gestionadas; emite `defect.reopened` en lugar de `defect.status_changed`.
- `duplicateOfUlid` debe apuntar a otro defecto del **mismo proyecto** cuando se usa.

Requiere el permiso de escritura de defectos (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden` antes de cualquier escritura.

Devuelve `404 not_found` si el defecto no pertenece a la organización activa.

## Eliminar

`DELETE /api/v1/defects/{defectUlid}`

Elimina el defecto de forma permanente. Responde `204` si tiene éxito; un `GET` posterior devuelve `404 not_found`.

## Vincular con resultados de prueba

Los defectos pueden vincularse a intentos inmutables de ejecución (filas de `test_results`). El par `(defecto, resultado)` es único; repetir la misma petición devuelve el vínculo existente con `200`.

### Crear o re-vincular

`POST /api/v1/defects/{defectUlid}/result-links`

Requiere el permiso de escritura de defectos (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`.

| Campo              | Obligatorio | Notas                                                        |
| ------------------ | ----------- | ------------------------------------------------------------ |
| `resultUlid`       | sí          | Resultado de una ejecución del mismo proyecto que el defecto |
| `stepSnapshotUlid` | no          | Instantánea de paso congelada perteneciente a ese resultado  |

Responde `201` en el primer vínculo y `200` si el par ya existía.

### Listar ocurrencias

`GET /api/v1/defects/{defectUlid}/result-links`

Paginación por cursor (`limit`, `cursor`). Cada ítem incluye el vínculo, el autor, el estado/`executedAt` del resultado y el contexto de ejecución y caso.

### Desvincular

`DELETE /api/v1/defects/{defectUlid}/result-links/{linkUlid}`

Requiere el permiso de escritura de defectos (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`. Responde `204`. El vínculo debe pertenecer al defecto indicado.

### Sugerencias para un caso de prueba

`GET /api/v1/test-cases/{caseUlid}/defects`

Lista defectos vinculados previamente a cualquier resultado del caso, con `occurrenceCount` y `lastSeenAt` acotados al caso. Los defectos abiertos o en progreso aparecen antes que los resueltos o cerrados.

## Casos de prueba afectados

`GET /api/v1/defects/{defectUlid}/affected-cases`

Paginación por cursor (`limit`, `cursor`). Devuelve casos de prueba distintos vinculados mediante asociaciones de resultado. Cada ítem incluye `testCaseUlid`, `testCaseTitle`, `caseNumber`, `displayId` (la referencia del caso en el repositorio, `{PROJECT_CODE}-{caseNumber}`), `occurrenceCount`, `lastLinkedAt`, `latestResult` (la ejecución más reciente del caso en general, o `null` si nunca se ejecutó) y `verification`.

Cuando el estado del defecto es `resolved`, `verification` puede ser:

| Valor                  | Significado                                                               |
| ---------------------- | ------------------------------------------------------------------------- |
| `verified`             | El último resultado es `passed` con `executedAt` posterior a `resolvedAt` |
| `failed_after_resolve` | El último resultado es `failed` o `blocked` después de `resolvedAt`       |
| `pending`              | No hay ejecución cualificada tras la resolución                           |

Para defectos no resueltos, `verification` es `null`. Devuelve `404 not_found` entre inquilinos.

## Eventos de auditoría

Las mutaciones emiten `defect.created`, `defect.updated`, `defect.status_changed`, `defect.reopened`, `defect.deleted`, `defect.linked` y `defect.unlinked` en el feed de actividad de la organización (los eventos de vínculo también aparecen en el feed de la ejecución mediante `run_id`). Las mutaciones de evidencia y discusión también emiten `defect.attachment_added`, `defect.attachment_deleted`, `defect.commented`, `defect.comment_updated` y `defect.comment_deleted`. Las resoluciones duplicado incluyen `duplicateOfUlid` y una etiqueta `D-<n>` en los metadatos de `defect.status_changed`. Ver [Eventos de auditoría](https://docs.probara.net/es/developers/api/audit-events/).

### Historial por defecto

`GET /api/v1/defects/{defectUlid}/audit-events`

Paginación por cursor (`limit`, `cursor`) sobre eventos con `entityType` `defect` y `entityUlid` igual al defecto. La respuesta coincide con el endpoint de auditoría de ejecuciones (`events`, `nextCursor`). Devuelve `404 not_found` si el defecto no pertenece a la organización activa.

## Adjuntos de evidencia

Los archivos de evidencia se almacenan bajo el prefijo R2 de la organización. Se acepta cualquier tipo salvo ejecutables y scripts (`application/x-msdownload`, `application/x-msdos-program`, `application/x-sh`, `application/x-bat`, `application/x-msi`, `application/x-executable`, `application/vnd.microsoft.portable-executable`), comprobado contra el tipo de contenido declarado de cada parte; los archivos vacíos se rechazan. Las imágenes PNG, JPEG y WebP se convierten a WebP con miniatura y vuelven con `disposition: "inline"`; su límite es de **10 MiB** y 8192 px por lado. Cualquier otro archivo se guarda tal cual, hasta **32 MiB**, con `disposition: "attachment"`. Hasta **20** partes `file` por solicitud de subida, y un defecto admite como máximo **20** adjuntos: una solicitud que lo superaría se rechaza entera. Las subidas cargan el contador de almacenamiento `defect_attachment` de la organización.

Los visualizadores pueden listar adjuntos; solo **miembros y roles superiores** pueden subir o eliminar. Subir y eliminar requieren el permiso de ejecución de adjuntos (que tienen `owner`/`admin`/`member`); un `viewer` recibe `403 forbidden`.

### Listar

`GET /api/v1/defects/{defectUlid}/attachments`

Devuelve `{ attachments: [...] }`. Cada archivo, en orden de subida (más antiguos primero), tiene `kind` `file` y `ulid`, `mime`, dimensiones, `byteSize`, `originalFilename`, `uploadedBy`, `url`, `thumbUrl` y `createdAt`.

La lista también incluye las imágenes de los textos del defecto: su descripción y los valores de sus campos personalizados de párrafo. Cada una tiene `kind` `image` y un `field` que indica en qué texto está: `{ "kind": "description" }`, o `{ "kind": "custom_field", "fieldUlid": "01J...", "name": "..." }`. Una imagen sale de la lista cuando sale de su texto.

### Subir

`POST /api/v1/defects/{defectUlid}/attachments`

`multipart/form-data` con una o más partes `file`. Devuelve `201` con `{ attachments: [...] }`. Un tipo bloqueado, un archivo o imagen demasiado grande, un archivo vacío o demasiados archivos devuelven `422 validation_failed`. Emite `defect.attachment_added`.

Es una subida directa, no una preparación, e ignora la cabecera [`Idempotency-Key`](https://docs.probara.net/es/developers/api/idempotency/). Un reintento puede guardar los mismos archivos dos veces: tras un timeout u otro fallo ambiguo, lista los adjuntos del defecto antes de reintentar. Mantén el cuerpo completo de la petición en 64 MiB como máximo (`ATTACHMENT_UPLOAD_REQUEST_RECOMMENDED_MAX_BYTES`) y reparte los lotes más grandes en varias peticiones. Cloudflare rechaza un cuerpo de más de 100 MB con `413` antes de que llegue a la API, permitan lo que permitan los límites por archivo y por petición; ese `413` no lleva el envoltorio de error de la API.

### Eliminar

`DELETE /api/v1/defects/{defectUlid}/attachments/{attachmentUlid}`

Devuelve `204`. Elimina la fila, el archivo guardado y su miniatura si la tiene (imágenes PNG, JPEG y WebP), y reembolsa bytes de almacenamiento. Emite `defect.attachment_deleted`. Un ULID de adjunto de otro defecto devuelve `404 not_found`.

Al eliminar un defecto se quitan todos los objetos de evidencia y se reembolsan los bytes antes del cascade.

## Comentarios de discusión

Los comentarios son texto enriquecido (Markdown en `body` o un documento en `bodyDocument`, máximo **5000** caracteres de texto) y admiten un nivel de respuestas en hilo, fijado y resolución de menciones `@`. La superficie de comentarios de defectos es estructuralmente idéntica a los [comentarios de casos de prueba](https://docs.probara.net/es/developers/api/test-case-comments/) — consulta esa página para la referencia completa (semántica de respuestas, reglas de fijado, resolución de menciones, forma de la respuesta y eliminación en cascada). Esta sección solo lista las rutas y acciones de auditoría específicas de defectos.

Los visualizadores pueden listar comentarios; solo **miembros y roles superiores** pueden crear, editar, eliminar o fijar/desfijar. El listado usa paginación por cursor sobre comentarios de **primer nivel** (`parentUlid` ausente), con orden predeterminado del más antiguo al más reciente; el arreglo `replies[]` de cada elemento se mantiene cronológico sin importar el orden.

### Listar

`GET /api/v1/defects/{defectUlid}/comments`

Consulta: `limit` (predeterminado `50`, máximo `200`), `cursor`, `sort` (`oldest` | `newest`, predeterminado `oldest`). Cada elemento lleva `parentUlid`, `pinnedAt`, `pinnedBy`, `replyCount`, `mentions`, y `replies[]` en línea; el `pinned[]` de la respuesta contiene el conjunto completo de fijados para el defecto. Las respuestas **no** se enumeran en el `items[]` plano — ver [comentarios de casos de prueba](https://docs.probara.net/es/developers/api/test-case-comments/) para la forma completa de la respuesta y la advertencia sobre las respuestas.

### Crear

`POST /api/v1/defects/{defectUlid}/comments`

| Campo        | Obligatorio | Notas                                                                                       |
| ------------ | ----------- | ------------------------------------------------------------------------------------------- |
| `body`       | sí          | Texto recortado, 1–5000 caracteres                                                          |
| `parentUlid` | no          | Responde a un comentario de primer nivel existente en el mismo defecto (solo profundidad 1) |

Devuelve `201`. Emite `defect.commented` con un extracto de 140 caracteres.

### Editar

`PATCH /api/v1/defects/{defectUlid}/comments/{commentUlid}`

| Campo  | Obligatorio | Notas                         |
| ------ | ----------- | ----------------------------- |
| `body` | sí          | Misma validación que en crear |

**Solo el autor** — los administradores no pueden editar el comentario de otro miembro (`403 forbidden`). Establece `editedAt` al tener éxito y vuelve a resolver las menciones. Emite `defect.comment_updated`.

### Eliminar

`DELETE /api/v1/defects/{defectUlid}/comments/{commentUlid}`

Devuelve `204`. El **autor** puede eliminar su propio comentario. Los **administradores de la organización** pueden eliminar cualquier comentario. Otros miembros reciben `403`. Eliminar un comentario de primer nivel elimina en cascada sus respuestas, purgando sus adjuntos. Emite `defect.comment_deleted` (una vez por el comentario eliminado, más una vez por cada respuesta eliminada en cascada).

### Fijar / desfijar

`POST` / `DELETE /api/v1/defects/{defectUlid}/comments/{commentUlid}/pin`

Cualquier miembro o rol superior puede fijar o desfijar cualquier comentario de **primer nivel**, sin importar la autoría; los visualizadores reciben `403`. Fijar una respuesta devuelve `422 validation_failed`. No hay límite de comentarios fijados por defecto. Emite `defect.comment_pinned` / `defect.comment_unpinned`.
