# Cómo las mutaciones de GraphQL para agentes de IA resisten llamadas destructivas

Las solicitudes GraphQL generadas fallan de una forma predecible: el modelo tiene información suficiente para producir una sintaxis válida, pero no la suficiente resistencia para que una acción peligrosa resulte difícil. Una mutación llamada `updateProject` con un campo `archived` opcional parece flexible para una persona. Para un agente que arma una solicitud con contexto parcial, es una invitación a cambiar el estado del proyecto como efecto secundario de una edición rutinaria.

Pon esa resistencia en el esquema y el resolver, no en una instrucción que pida al agente tener cuidado. Las entradas tipadas, una autoridad de acción limitada, vistas previas útiles, escrituras condicionales y errores con una ruta de recuperación hacen que sea más fácil generar solicitudes correctas que solicitudes dañinas. Este diseño también ayuda a quienes desarrollan clientes normales. Los agentes simplemente ponen al descubierto los atajos que las API poco estrictas han tolerado durante años.

## Una solicitud válida aún puede expresar la acción equivocada

GraphQL comprueba que una solicitud coincida con el esquema. No determina si quien llama seleccionó al cliente correcto, entendió el estado del registro o pretendía eliminar algo. Los equipos suelen confundir la seguridad de tipos con la seguridad de las acciones y luego colocan un campo llamado `delete`, `archive` o `status` dentro de una mutación de actualización amplia.

Considera este esquema habitual:

```graphql
input ProjectPatchInput {
  name: String
  description: String
  archived: Boolean
  ownerId: ID
}

type Mutation {
  updateProject(id: ID!, input: ProjectPatchInput!): Project!
}
```

Aquí se mezclan ediciones inofensivas con una transferencia de propiedad y una transición del ciclo de vida. Un agente al que se le pida «limpiar proyectos antiguos» puede deducir razonablemente que establecer `archived: true` es apropiado. Uno al que se le pida corregir el nombre de un proyecto puede conservar por accidente un campo `archived` de un objeto generado antes. El sistema de tipos acepta ambas solicitudes porque las dos están bien formadas.

No escondas el comportamiento destructivo dentro de un parche flexible. Da a cada acción un nombre que indique su consecuencia y una entrada que contenga solo las pruebas necesarias para esa consecuencia:

```graphql
type Mutation {
  renameProject(input: RenameProjectInput!): RenameProjectPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
  transferProjectOwnership(input: TransferProjectOwnershipInput!): TransferProjectOwnershipPayload!
}

input RenameProjectInput {
  projectId: ID!
  expectedVersion: Int!
  name: String!
}

input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}
```

Esto no es burocracia gratuita. Una mutación limitada reduce lo que una solicitud generada puede expresar. Crea una diferencia útil entre «editar una etiqueta» y «retirar esto del uso normal». Las descripciones de las herramientas pueden explicar la diferencia, pero el esquema debe hacerla cumplir.

La especificación de GraphQL ayuda aquí de forma limitada, aunque útil. La validación de objetos de entrada rechaza los campos que el esquema no define. Si `ArchiveProjectInput` no incluye `ownerId`, un cliente no puede introducir de contrabando un cambio de propietario en una llamada de archivado. Trátalo como una barrera de protección, no como un límite de seguridad. El resolver sigue decidiendo si el actor puede archivar ese proyecto concreto.

Evita un campo genérico `action: String!`, como `mutateProject(action: «ARCHIVE»)`. Parece compacto hasta que cada acción necesita campos, validación, autorización, datos de vista previa y gestión de errores diferentes. El resultado se convierte en un protocolo RPC privado atrapado dentro de un objeto de entrada, con menos ayuda de las herramientas de GraphQL.

## Las entradas deben indicar el objetivo y el límite

Una entrada destructiva debe identificar exactamente qué cambiará, qué versión examinó quien llama y qué límite impide que la selección se amplíe. Los IDs por sí solos no transmiten suficiente intención cuando un resolver puede extenderse a registros secundarios, sistemas externos o una consulta para todo un tenant.

Empieza con un objeto que identifique un único recurso dentro del tenant de quien llama. No aceptes un filtro arbitrario en una mutación de eliminación salvo que el producto necesite realmente operaciones masivas. Un filtro como `where: { status: INACTIVE }` invita a la ambigüedad: ¿inactivo según qué fecha, qué tenant y qué valor predeterminado oculto? El modelo puede proporcionarlo porque el campo existe, no porque haya revisado el conjunto resultante.

Para operaciones sobre un solo registro, incluye un token de versión en la entrada. Un entero es fácil de inspeccionar, aunque también sirve una cadena de revisión opaca. El resolver la compara con la versión almacenada en la misma transacción que escribe el cambio. Si difieren, devuelve un conflicto y no cambia nada.

```graphql
input ArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
  confirmation: String!
  idempotencyKey: String!
}

enum ArchiveReason {
  CUSTOMER_REQUEST
  DUPLICATE
  END_OF_LIFE
}
```

El enum `reason` hace algo más que mejorar los informes. Impide que una solicitud invente una justificación de texto libre que una automatización posterior pueda tratar como significativa. Usa texto libre para una nota cuando las personas lo necesiten, pero mantén enumerables las categorías operativas.

Un campo de confirmación debe vincularse al objetivo real. Exigir la cadena literal `ARCHIVE` solo detecta una construcción descuidada. Exigir `archive acme-project-42` obliga al cliente a resolver y repetir el identificador de un recurso. No detiene a un cliente malicioso y nunca debe sustituir a la autorización. Sí detecta muchas solicitudes generadas que asociaron la acción correcta con el ID equivocado.

No pidas confirmación en mutaciones rutinarias, como cambiar un nombre visible. El exceso de confirmaciones enseña a agentes y personas a rellenar todos los campos mecánicamente. Resérvala para acciones con un resultado importante o difícil de revertir: eliminación, publicación, movimiento de dinero, revocación de credenciales y cambios que afecten a otros usuarios.

En una operación masiva, haz explícito el límite superior y devuelve un token de vista previa vinculado a la selección exacta. Esta entrada dice mucho más que un filtro sin restricciones:

```graphql
input DeleteDormantProjectsInput {
  previewToken: ID!
  expectedCount: Int!
  confirmation: String!
  idempotencyKey: String!
}
```

El resolver de ejecución debe rechazar un token que haya caducado, pertenezca a otro actor, describa un tenant diferente o produzca un recuento distinto de `expectedCount`. De lo contrario, un agente puede previsualizar cinco registros y ejecutar una consulta cambiante que ahora coincida con cincuenta.

## La autoridad debe seguir a la mutación, no al sustantivo

Un alcance llamado `projects:write` suele ser demasiado amplio para el trabajo autónomo. Permite renombrar, archivar, transferir, eliminar y quizá modificar la configuración de facturación de un proyecto con un solo permiso, porque todas las acciones afectan a un proyecto. Esa agrupación sigue al sustantivo de la base de datos, no al riesgo de la operación.

Concede una autoridad que describa una acción. Por ejemplo, un token de servicio para la automatización de lanzamientos podría tener `project:rename` y `project:archive`, mientras que un flujo de soporte no tendría ninguno de los dos. Un alcance independiente `project:delete` debería ser poco habitual. Si tu sistema de identidad no puede emitir alcances tan limitados, añade una comprobación de capacidad en el servidor asociada al nombre de la mutación y regístrala en la decisión de autorización.

El alcance por sí solo nunca resuelve el acceso. Cada resolver necesita varias comprobaciones en un orden deliberado:

1. Autenticar a quien llama e identificar su tenant y principal.
2. Comprobar que el principal tiene autoridad para esta mutación.
3. Cargar el objetivo dentro del límite del tenant, en lugar de cargarlo globalmente y comprobarlo después.
4. Comprobar el estado del registro y cualquier relación de rol exigida por la regla de negocio.
5. Ejecutar la escritura condicional y añadir un evento de auditoría en la misma transacción.

Cargar dentro del límite del tenant es importante. Un resolver que llama a `findProjectById(id)` antes de verificar el tenant puede filtrar la existencia mediante el tiempo de respuesta o el texto del error. También puede entregar un objeto cargado globalmente a una función auxiliar que dé por hecha la autorización. Incluye la pertenencia al tenant en el predicado de búsqueda.

No deduzcas el permiso a partir de la tarea declarada por el agente. Un encabezado de solicitud que diga `X-Agent-Goal: cleanup` es una prueba para el registro de auditoría, no una concesión de autoridad. Las instrucciones, las etiquetas de tareas y la identidad del modelo pueden ayudar a una persona a revisar una acción, pero cualquier cliente puede falsificarlas.

La misma distinción se aplica al acceso a las herramientas. Un agente puede tener permiso para llamar a un endpoint de GraphQL y carecer de permiso para una mutación concreta. Cuando el entorno del agente lo permita, describe por separado las herramientas de lectura y las herramientas de acción. Mantén la decisión final en la API, porque un cliente puede saltarse los metadatos de la herramienta y enviar la solicitud HTTP directamente.

## Una simulación debe construir el mismo plan que la ejecución

Una simulación solo sirve si responde: «¿Qué haría exactamente esta solicitud en este momento?». Una vista previa falsa que cuente filas con una consulta simplificada da a los agentes una sensación de seguridad equivocada. La mutación final puede aplicar reglas de elegibilidad distintas, usar otra rama de autorización o activar una acción externa que la vista previa nunca tuvo en cuenta.

Construye una función de planificación compartida. Recibe al actor autenticado y la entrada, valida todas las condiciones, resuelve los objetivos, calcula los efectos secundarios y produce un plan inmutable. La vista previa devuelve una representación depurada de ese plan. La ruta de ejecución consume el plan solo después de que quien llama presente su token de corta duración y la confirmación.

```graphql
type Mutation {
  previewArchiveProject(input: PreviewArchiveProjectInput!): ArchivePreviewPayload!
  archiveProject(input: ArchiveProjectInput!): ArchiveProjectPayload!
}

input PreviewArchiveProjectInput {
  projectId: ID!
  expectedVersion: Int!
  reason: ArchiveReason!
}

type ArchivePreviewPayload {
  previewToken: ID!
  project: Project!
  affectedMemberCount: Int!
  plannedEffects: [ArchiveEffect!]!
  expiresAt: DateTime!
}

enum ArchiveEffect {
  PROJECT_HIDDEN_FROM_DEFAULT_LISTS
  PENDING_INVITATIONS_CANCELLED
}
```

Un plan debe incluir los IDs de los objetivos, sus versiones, la identidad del actor, el tenant, el resumen de la entrada, los efectos previstos y la hora de caducidad. Guárdalo en el servidor o firma un token opaco que haga referencia al estado almacenado. No introduzcas el plan completo en un objeto JSON controlado por el cliente para luego confiar en él durante la ejecución.

La entrada de ejecución debe referirse al token de vista previa, no repetir un selector flexible:

```graphql
input ArchiveProjectInput {
  previewToken: ID!
  confirmation: String!
  idempotencyKey: String!
}
```

Este flujo de dos llamadas añade fricción. Ese es el objetivo en las acciones importantes. No lo impongas en todas las mutaciones. Una regla sencilla funciona bien: exige una vista previa cuando una operación afecte a más de un registro, tenga un efecto externo irreversible o deje un recurso no disponible para otros usuarios.

Las respuestas de vista previa también necesitan control de acceso. Devolver una lista de registros afectados puede filtrar datos con la misma facilidad que ejecutar la mutación. Aplica las mismas reglas de tenant y rol durante la planificación. Una vista previa puede omitir campos que el actor no puede leer y aun así devolver el recuento y las categorías de efectos necesarios para decidir.

## La idempotencia y las versiones resuelven fallos distintos

La idempotencia evita aplicar dos veces la misma solicitud. Una comprobación de versión evita aplicar una acción sobre un estado que cambió después de que quien llama lo inspeccionara. Los equipos suelen añadir una de las dos y asumir que han resuelto ambos problemas.

Un agente puede reintentar porque la conexión HTTP se cerró después de que el servidor confirmara una mutación. Sin idempotencia, la segunda solicitud puede crear un segundo reembolso, duplicar un mensaje o llamar dos veces a la misma API externa. Da a cada mutación con efectos una `idempotencyKey` proporcionada por quien llama. El servidor debe guardarla junto con el actor autenticado, el nombre de la mutación, un resumen de la entrada normalizada y la respuesta completada o el error estable.

Cuando el servidor ve de nuevo el mismo actor, mutación, clave y resumen de entrada, devuelve el resultado original. Si ve la misma clave con un resumen diferente, devuelve `IDEMPOTENCY_KEY_REUSED` y no hace nada. Aceptar una entrada modificada con una clave reutilizada destruye la propiedad en la que los clientes confían durante los reintentos.

Una comprobación de versión gestiona otra secuencia. Un agente lee la versión 7 de un proyecto, prepara una vista previa de archivado y una persona cambia el nombre del proyecto o restaura una invitación. Cuando se ejecuta el archivado, el resolver compara la versión 7 con la versión almacenada actual. Si ahora es 8, devuelve un conflicto. El agente debe leer el estado actual, reconsiderar su intención y crear una vista previa nueva si hace falta.

La especificación de GraphQL ejecuta en serie los campos de nivel superior de una operación de mutación. Ese orden no serializa solicitudes HTTP independientes. Dos agentes aún pueden enviar dos operaciones de mutación casi al mismo tiempo. Usa una actualización condicional de la base de datos, un bloqueo de fila o una restricción transaccional. Una comprobación en la memoria de la aplicación seguida de una escritura separada deja una ventana de carrera.

Una actualización condicional con forma de SQL deja clara la invariante buscada:

```sql
UPDATE projects
SET archived_at = CURRENT_TIMESTAMP,
    version = version + 1
WHERE id = :project_id
  AND tenant_id = :tenant_id
  AND version = :expected_version
  AND archived_at IS NULL;
```

Si esto afecta a cero filas, inspecciona el registro actual dentro del límite del tenant y devuelve un resultado específico: inexistente, no permitido, ya archivado o conflicto de versión. No informes de cada resultado de cero filas como un error genérico del servidor. Los agentes necesitan saber si reintentar es perjudicial, útil o inútil.

## Las respuestas de error deben indicar al agente qué hacer después

El arreglo de nivel superior `errors` de GraphQL sirve para errores de análisis, fallos de validación y errores del resolver. Es un mal lugar para obligar a los clientes a extraer resultados de negocio de mensajes en inglés. Incluye los resultados esperados de las mutaciones en una respuesta tipada con un código estable y detalles estructurados.

```graphql
type ArchiveProjectPayload {
  outcome: ArchiveProjectOutcome!
  project: Project
  error: MutationError
}

enum ArchiveProjectOutcome {
  ARCHIVED
  VERSION_CONFLICT
  CONFIRMATION_REQUIRED
  PREVIEW_EXPIRED
  FORBIDDEN
  IDEMPOTENCY_KEY_REUSED
}

type MutationError {
  code: String!
  message: String!
  currentVersion: Int
  requiredConfirmation: String
}
```

Usa errores de transporte y de ejecución de GraphQL cuando el cliente no haya podido ejecutar correctamente la operación. Usa un resultado tipado cuando una solicitud se haya ejecutado con normalidad, pero no haya cambiado el estado porque una regla de negocio la rechazó. Elige una convención y documéntala. Mezclar `errors.extensions.code` para algunos conflictos con enums de respuesta para otros vuelve frágil el comportamiento del agente.

Un agente debe poder asociar cada resultado con una acción segura. `VERSION_CONFLICT` significa volver a leer el objeto y reconsiderar. `PREVIEW_EXPIRED` significa crear una vista previa nueva. `CONFIRMATION_REQUIRED` significa mostrar la frase requerida a una persona o pedírsela, no adivinarla. `FORBIDDEN` significa detenerse. `IDEMPOTENCY_KEY_REUSED` significa generar una clave nueva solo después de que quien llama indique explícitamente que pretende realizar una operación distinta.

No devuelvas nombres de políticas internas, fragmentos SQL ni detalles del grafo de autorización. Los códigos externos estables pueden ser precisos sin exponer detalles de implementación. Conserva un ID de correlación en las extensiones de la respuesta y un registro de auditoría correspondiente en el servidor. Así, el operador tendrá algo concreto que investigar cuando un agente informe de un fallo.

Las respuestas satisfactorias necesitan suficiente información para poner fin a la incertidumbre. Devuelve el estado resultante del registro, la nueva versión, el ID de la operación y los efectos que realmente ocurrieron. Un booleano aislado obliga al cliente a hacer otra consulta y deja margen para una lectura obsoleta. También dificulta mucho más la revisión humana.

## La eliminación necesita un ciclo de vida, no un booleano

La eliminación definitiva es popular porque deja la tabla ordenada. También es la acción con más probabilidades de provocar un problema de soporte irrecuperable cuando un agente interpreta mal una solicitud. Muchos productos deberían archivar primero, conservar un periodo de deshacer controlado por el servidor y realizar la eliminación definitiva mediante un flujo restringido independiente.

No llames `deleteProject` a una operación de archivado si solo oculta un registro. Los nombres enseñan a los clientes qué estado pueden esperar. `archiveProject` debe devolver `ARCHIVED`; `purgeProject` debe significar que los datos dejarán de estar disponibles. Cuando una API usa delete para todas las etapas del ciclo de vida, un agente no puede distinguir de forma fiable entre una limpieza reversible y una eliminación permanente.

Una eliminación definitiva necesita una entrada y una autorización más estrictas que un archivado. Puede exigir que el recurso haya permanecido archivado durante un periodo de retención, que no exista una retención legal o de facturación y que un operador con un alcance distinto la apruebe. El resolver debe hacer cumplir cada condición. Una cuenta atrás en el cliente o una instrucción de la herramienta no tienen autoridad.

Los efectos secundarios externos merecen el mismo tratamiento. Si archivar cancela invitaciones, elimina un entorno remoto o activa un webhook, devuelve esos efectos en la vista previa y en la respuesta final. No los adjuntes en silencio a un resolver de actualización genérico. La persona que revise una solicitud del agente debe ver las consecuencias antes de aprobarla, y el agente necesita datos que pueda comunicar después de ejecutarla.

En acciones financieras o relacionadas con credenciales, no ofrezcas una simulación ficticia que llame al endpoint activo del proveedor y espere no producir efectos. Usa la vista previa o el mecanismo de autorización documentado por el proveedor cuando exista. De lo contrario, presenta el resultado como una estimación local y enumera lo que el servidor no pudo verificar. Fingir certeza es peor que pedir una decisión humana.

## Las comprobaciones del resolver hacen realidad las promesas del esquema

El diseño del esquema limita la intención mal formada. El diseño del resolver evita que una solicitud que parece autorizada cruce un límite real. Mantén esas capas separadas en el código para que una futura refactorización no sustituya una comprobación de permisos por un comentario en la definición de una herramienta.

Un resolver para una acción destructiva debe seguir una secuencia que haga barato el rechazo y deje las escrituras para el final. Primero autentica la solicitud, resuelve el tenant del actor, valida la entrada, carga el objetivo dentro de ese tenant, verifica el alcance y el estado, valida el token de vista previa y la confirmación, reclama el registro de idempotencia y ejecuta la transacción condicional. El orden puede cambiar según tu modelo de almacenamiento, pero no produzcas un efecto externo antes de saber que la transacción puede confirmarse.

Un registro de idempotencia necesita un tratamiento cuidadoso cuando el trabajo abarca una base de datos y un proveedor externo. Marcar una clave como completada antes de la llamada externa puede declarar éxito cuando la llamada falló. Llamar primero al proveedor puede duplicar la operación si el proceso muere antes de guardar la finalización. Usa un patrón outbox o la compatibilidad con idempotencia del proveedor cuando exista. Registra una operación pendiente duradera, confirma la decisión local y envía el efecto externo con un ID de operación que sobreviva a los reintentos.

Los registros de auditoría deben identificar al principal autenticado, la ejecución del agente cuando corresponda, el nombre de la mutación, el objetivo normalizado, el resumen de la entrada, el resultado de la autorización, la referencia de la vista previa, la clave de idempotencia, el resultado y la versión resultante. Redacta las notas y los campos que contengan información sensible de acuerdo con tus reglas de conservación. Un evento de auditoría que solo diga «mutación completada» sirve de muy poco durante un incidente.

En el caso de agentes que actúan mediante llamadas HTTP o SSH con credenciales, mantén las credenciales fuera del proceso del modelo siempre que puedas. Sallyport enruta las acciones compatibles mediante su bóveda local y registra cada llamada, algo útil cuando una mutación de GraphQL necesita una autorización visible para una persona además de la que proporciona un token bearer.

## Prueba la solicitud generada, no solo el resolver

Las pruebas unitarias que llaman a un resolver con objetos construidos cuidadosamente no detectan el fallo que te interesa. Los clientes generados envían campos omitidos, valores nulos, IDs obsoletos, alias, solicitudes repetidas y variables construidas a partir de resultados de herramientas anteriores. Prueba el límite público de GraphQL con las mismas formas.

Construye una matriz de pruebas de mutaciones centrada en el comportamiento y no en las ramas del código. Como mínimo, cubre un autor de llamada de otro tenant, uno con acceso de lectura pero sin alcance de acción, una vista previa caducada, una versión del objetivo modificada, una cadena de confirmación incorrecta, una clave de idempotencia repetida y dos llamadas simultáneas con la misma versión esperada. Comprueba tanto la respuesta como el estado persistente después de cada prueba.

Esta solicitud debe fallar durante la validación de GraphQL porque la entrada no define `ownerId`:

```graphql
mutation BadArchive($input: ArchiveProjectInput!) {
  archiveProject(input: $input) {
    outcome
  }
}
```

```json
{
  "input": {
    "projectId": "prj_42",
    "expectedVersion": 7,
    "reason": "DUPLICATE",
    "confirmation": "archive prj_42",
    "idempotencyKey": "run-18-archive-42",
    "ownerId": "usr_9"
  }
}
```

La respuesta esperada pertenece al formato de errores de GraphQL de nivel superior porque el documento proporcionó un objeto de entrada no válido. Esa prueba demuestra que el esquema mantiene fuera de la mutación las capacidades no relacionadas. Otra prueba debe demostrar que una solicitud de archivado bien formada también falla cuando el actor pertenece a otro tenant.

Ejecuta las pruebas de concurrencia contra el comportamiento transaccional real, no contra un sustituto en memoria. Envía dos solicitudes de archivado con el mismo ID y la misma versión esperada, y comprueba que una devuelva `ARCHIVED` y la otra un conflicto o un resultado de repetición idempotente. Si ambas llamadas informan de éxito con distintos IDs de operación, la escritura condicional no está cumpliendo su función.

Prueba también la verificación de auditoría. Si tu puerta de enlace de acciones produce un registro de auditoría cifrado y resistente a manipulaciones, incluye su verificación en los simulacros de incidentes en lugar de dejarla como un comando que nadie ha usado. Sallyport expone `sp audit verify` para verificar sin conexión su cadena de hashes sin una clave de bóveda. Ejecútalo contra un diario copiado y asegúrate de que los operadores sepan qué significa un fallo de verificación.

## Las herramientas generadas necesitan menos opciones, no advertencias más largas

Los agentes funcionan mejor cuando el esquema de una herramienta presenta la acción segura más pequeña que corresponde a la tarea. Un catálogo enorme de mutaciones con filtros genéricos, indicadores y efectos secundarios opcionales obliga al modelo a inferir la política a partir de los nombres de los campos. Un catálogo compacto de operaciones explícitas de lectura, vista previa, ejecución y recuperación le ofrece un camino que puede seguir.

Expón operaciones de lectura que devuelvan los identificadores, versiones, estados y nombres que el agente necesita antes de proponer una mutación. Si el agente tiene que inventar un ID a partir de una etiqueta humana, el diseño de la mutación no podrá salvarte. Devuelve los IDs estables de forma clara y haz explícitos los resultados de búsqueda ambiguos en lugar de elegir uno en silencio.

Escribe descripciones de herramientas que indiquen una condición previa y una consecuencia, pero mantén el servidor como punto de cumplimiento. Por ejemplo: «Archiva un proyecto después de una vista previa correcta. Cancela las invitaciones pendientes que aparecen en la vista previa». Es mejor que «Usar con cuidado», que no dice nada operativo a un agente.

No intentes resolver todos los riesgos añadiendo un diálogo de aprobación humana. La aprobación es adecuada cuando una persona es responsable de la decisión, pero las solicitudes repetitivas se convierten en ruido de fondo. Pon la protección rutinaria en los alcances, las comprobaciones de tenant, las versiones y la idempotencia. Pide a una persona que revise el pequeño conjunto de acciones cuya intención no puede inferirse de los datos, cuya consecuencia es permanente o que cruza un límite organizativo.

Empieza por la mutación que más daño causaría si un agente la llamara dos veces, la llamara con un estado obsoleto o la dirigiera al tenant equivocado. Divide su entrada, añade una vista previa real cuando la acción lo justifique, haz que la escritura sea condicional y escribe la prueba de repetición. Ese trabajo mostrará si tu API modela claramente una acción o si solo expone campos de la base de datos.
