# API de archivo de proyectos

> Archiva y desarchiva un proyecto, y entiende el contrato de la lista activa por defecto, mediante la API para integradores.

Cada proyecto tiene una marca de tiempo `archivedAt` (epoch en ms, o `null` para un proyecto activo) — un
estado de ciclo de vida **reversible**, no una eliminación suave. `{projectId}` es el **código** del
proyecto (por ejemplo `ACME`) en todo lo que sigue.

Archivar hace que el **contenido** de un proyecto sea de solo lectura, mientras su
**administración** sigue siendo accesible:

- Cada ruta existente sigue siendo totalmente **legible**: la página de detalle, las suites,
  casos, ejecuciones, planes, hitos, defectos, adjuntos y exportaciones de un proyecto archivado
  siguen funcionando exactamente igual que en un proyecto activo. `GET /api/v1/projects/{projectId}`
  sigue devolviendo `200`, con un `archivedAt` no nulo.
- Cada **escritura sobre el contenido** es rechazada con `409 { code: "project_archived" }` — ver
  [Las escrituras de contenido se rechazan mientras está archivado](#las-escrituras-de-contenido-se-rechazan-mientras-está-archivado)
  más abajo.
- **No** libera un asiento de proyecto. Una organización en su `projectLimit` que archiva un proyecto
  sigue siendo rechazada al crear uno nuevo con `409 { code: "project_limit_exceeded" }` — eliminar sigue
  siendo la única forma de liberar un asiento.

## Cambio disruptivo: la lista muestra solo activos por defecto

**`GET /api/v1/projects` ahora oculta los proyectos archivados por defecto.** Este es un cambio
deliberado a un contrato público ya publicado: un integrador que sondea este endpoint empieza a recibir
menos filas en cuanto algo se archiva en esa organización. Es observacionalmente inerte el día en que se
publica, porque ningún proyecto puede archivarse hasta que esta capacidad exista — pero surte efecto de
inmediato en cuanto tú (o alguien de tu organización) archiva un proyecto.

Pasa el parámetro de consulta opcional `status` para cambiar lo que devuelve la lista:

| Valor de `status`                         | Resultado                                     |
| ----------------------------------------- | --------------------------------------------- |
| ausente, vacío o solo tokens desconocidos | **Solo activos** — el nuevo valor por defecto |
| `active`                                  | Solo activos (igual que el valor por defecto) |
| `archived`                                | Solo archivados                               |
| `active,archived` (en cualquier orden)    | Ambos — el comportamiento previo completo     |

`status` es un conjunto de tokens separados por comas, igual que los filtros `runs`/`defects`/
`milestones`/`cases` que ya existen en este endpoint. Se combina con la paginación, `q`, `memberUlid` y
cualquier otro filtro, y el `total` devuelto siempre coincide con el filtro aplicado (nunca un conteo
fantasma de una consulta sin filtrar).

```bash
# Solo lo que cambió con el flip: proyectos archivados
curl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects?status=archived"

# Todo, exactamente como antes de este cambio
curl -sS -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects?status=active,archived"
```

Cualquier otro consumidor de la lista de proyectos hereda este mismo valor por defecto sin **ningún**
trabajo extra de tu parte: el directorio web, el selector de proyectos y cualquier otro lector interno
resuelven a solo-activos a menos que también pasen `status` explícitamente.

## Archivar un proyecto

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

Sin cuerpo de solicitud. Devuelve `200` con el proyecto actualizado (ver
[Acceso a proyectos](https://docs.probara.net/es/developers/api/project-access/) para el recurso hermano; este es el objeto de
proyecto plano con su campo `archivedAt`).

```json
{
  "ulid": "01J...PROJECT",
  "name": "Acme",
  "id": "ACME",
  "description": null,
  "avatarKey": null,
  "avatarVersion": 0,
  "createdAt": 1700000000000,
  "updatedAt": 1700000005000,
  "archivedAt": 1700000005000
}
```

**Idempotente**: archivar un proyecto ya archivado responde `200` sin cambiar la marca de tiempo
almacenada — un doble clic o una solicitud reintentada se comportan igual que una sola llamada, y este
endpoint nunca responde `409`.

### Autorización

Misma restricción que `PATCH`/`DELETE` sobre un proyecto: solo un **propietario** o **administrador** de
la organización puede archivar. Un `member` o `viewer` es rechazado con `403 forbidden`, dejando
`archivedAt` sin cambios. Un `{projectId}` desconocido, o uno que pertenece a otra organización, responde
`404 not_found`.

## Desarchivar un proyecto

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

Sin cuerpo de solicitud. Devuelve `200` con el proyecto actualizado, `archivedAt: null`. Mismas reglas de
idempotencia y autorización que archivar, en espejo: desarchivar un proyecto que no está archivado tiene
éxito y deja `archivedAt` como `null`.

### Ejemplo

```bash
curl -sS -X POST \
  -H "Authorization: Bearer $PROBARA_API_TOKEN" \
  "https://app.probara.net/api/v1/projects/ACME/archive"
```

## Las escrituras de contenido se rechazan mientras está archivado

El contenido de un proyecto archivado queda congelado. Cualquier escritura sobre él — crear una
suite, actualizar un caso de prueba, iniciar una ejecución, guardar el resultado de una ejecución,
vincular un defecto, mutar un plan de pruebas, escribir el valor de un campo personalizado, crear
un comentario, subir un adjunto o un avatar, y cualquier otra mutación de contenido con alcance de
proyecto — es rechazada:

```json
{
  "error": {
    "code": "project_archived",
    "message": "project is archived"
  }
}
```

**Estado `409`.** Esto aplica tanto si la escritura apunta directamente al proyecto
(`/api/v1/projects/{projectId}/...`) como si apunta a una entidad alcanzada por su propio ULID
(`/api/v1/test-cases/{ulid}`, `/api/v1/runs/{ulid}`, etc.) — el rechazo se aplica donde vive el
dato, no donde apunta la URL, así que no existe ruta que lo evite. Una subida de adjunto rechazada
de esta forma nunca llega al almacenamiento: no queda ningún objeto parcial u huérfano. Esto aplica
a todo tipo de credencial, incluido un token de API de la organización.

El orden del rechazo es fijo: un proyecto que no existe (o pertenece a otra organización) responde
`404` antes de consultar el estado de archivado; un llamador sin acceso a un proyecto privado
responde `403`; solo un llamador que de otro modo habría sido admitido llega al `409`. El estado
de archivado nunca filtra la existencia de un proyecto que el llamador no puede ver.

Este es un cambio disruptivo a un contrato público ya publicado, y a diferencia del cambio de
valor por defecto de la lista anterior, **no es observacionalmente inerte**: cualquier proyecto
archivado antes de este cambio empieza a rechazar escrituras en el momento en que se publica.

### Operaciones administrativas exentas

Estas operaciones administran el proyecto en sí, no su contenido, y siguen funcionando mientras el
proyecto está archivado:

| Operación                                      | Ruta                                                                        |
| ---------------------------------------------- | --------------------------------------------------------------------------- |
| Desarchivar                                    | `POST /api/v1/projects/{projectId}/unarchive`                               |
| Eliminar                                       | `DELETE /api/v1/projects/{projectId}`                                       |
| Archivar (re-archivado idempotente)            | `POST /api/v1/projects/{projectId}/archive`                                 |
| Actualizar (renombrar/redescribir)             | `PATCH /api/v1/projects/{projectId}`                                        |
| Gestión de acceso (modo, membresía, propiedad) | `PATCH /api/v1/projects/{projectId}/access` y sus subrutas de miembro/grupo |

El **avatar** del proyecto es contenido, no administración, y **no** está exento:
`POST`/`DELETE /api/v1/projects/{projectId}/avatar` se rechazan con `409` como cualquier otra
escritura.

### Reintentos idempotentes

Una escritura rechazada con `409 project_archived` bajo una `Idempotency-Key` se registra como
cualquier otra respuesta por debajo de `500`, así que un reintento idéntico dentro del TTL de la
clave repite el mismo `409` registrado — incluso después de que el proyecto haya sido
desarchivado. Envía una clave nueva y sin usar para obtener el resultado posterior al
desarchivado. Ver [Idempotencia](https://docs.probara.net/es/developers/api/idempotency/) para el contrato general.

## Limitación conocida

La eliminación definitiva (`DELETE /api/v1/projects/{projectId}`) no tiene relación con archivar y sigue
siendo la única vía de eliminación irreversible. Eliminar no libera espacio de almacenamiento — la imagen
del proyecto y cada archivo descendiente sobreviven a la cascada. Esto es preexistente, está fuera
del alcance aquí, y se rastrea como su propio cambio futuro.
