You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

139 lines
14 KiB

# Modelos de datos compartidos — Go, Svelte y Flutter
**Estado:** contrato base definido el 6 de octubre de 2026 por indicación del usuario. Hay una fuente JSON Schema, DTOs generados TypeScript/Go/Dart y fixtures comprobables. Esto no implementa sesiones, salas, WSS ni una app Flutter. Los adaptadores de error del runtime y los contratos completos de esos flujos siguen pendientes.
## 1. Una fuente para los tres ámbitos
Los datos que cruzan una frontera se definen en [contracts/shared/schema.json](../../contracts/shared/schema.json), con JSON Schema 2020-12. De esa fuente se generan [TypeScript](../../contracts/shared/ts/models.generated.ts), [Go](../../contracts/shared/go/models.gen.go) y [Dart](../../contracts/shared/dart/models.g.dart). No se mantienen tres definiciones manuales del mismo DTO. Los nombres JSON, tipos, campos obligatorios, ausencia/null, códigos, unidades y significado deben ser iguales.
Un DTO compartido no es una entidad de base de datos, un componente Svelte ni un widget Flutter. Go mantiene entidades y causas internas separadas y proyecta el DTO autorizado; web y app lo decodifican y lo representan. Ningún campo del DTO acredita actor, edad o permiso. La lectura con otro lenguaje no cambia la información que recibe un mismo destinatario autorizado.
| Familia definida | Modelos y reglas |
| --- | --- |
| Identidad y versión | `UUID`: representación canónica minúscula; `Revision`: entero entre 0 y 2^53−1, sin pérdida de precisión JSON. Un UUID no concede autorización. |
| Tiempo e idioma | `Instant`: RFC 3339 UTC con `Z`; `Locale`: etiqueta estructural BCP 47. El patrón de idioma no sustituye canonicalización, soporte real ni revisión de traducciones. |
| Presentación de mensajes | `LocalizedMessage { key, args }`; argumentos escalares acotados, sin HTML, objetos anidados ni valores secretos. Misma clave/argumentos para cada SDK; traducción según preferencia. |
| Errores | `ErrorData`, `ErrorCategory`, `ErrorOrigin`, `OperationOutcome`, `Recovery`, `Violation`; núcleo único en todos los transportes. |
| Correlación y transporte | `Correlation`, `HttpProblem`, `ErrorMessagePayload`, `CommandRejection`; contexto separado del error duradero. |
| Catálogo | `GameCard`, `CatalogPage`, `CatalogResponse`; nombres y valores compatibles con la base actual. Antes de adoptar el límite de 100 fichas se materializa paginación en servidor. |
Los valores enteros deben seguir el rango seguro aunque Go/Dart dispongan de enteros más amplios. `NaN`/infinito no son datos JSON admitidos. Listas y mapas vacíos se emiten como `[]` y `{}`, no `null`; en Go deben inicializarse. Un campo opcional se omite; `null` solo se acepta donde el esquema lo declara, como `nextCursor`. Los instantes UTC son distintos de los intervalos monotónicos `*Ms` ligados a `clockEpoch` del [perfil temporal](../game-engine/synchronization-profile.md#4-reloj-medición-y-autoridad).
Los modelos de cuentas, salas, admisión, operaciones, recibos completos y proyecciones se incorporarán por cortes, desde los documentos que definen su dominio. Una clase con nombre parecido no convierte esos flujos pendientes en implementados. Cada campo nuevo debe declarar destinatarios y tratamiento de privacidad además de su tipo.
## 2. Núcleo común del error
Todos los errores públicos usan `ErrorData`, sin un formato diferente para Svelte, Go o Flutter:
```json
{
"modelVersion": 1,
"origin": "server",
"code": "ROOM_FULL",
"category": "conflict",
"outcome": "rejected",
"message": { "key": "errors.roomFull", "args": {} },
"recovery": { "action": "change-selection" },
"violations": []
}
```
| Campo | Información que comparte |
| --- | --- |
| `modelVersion` | Versión del modelo, independiente de `/api/v2`, protocolo de juego y versión de renderer. |
| `origin` | `server`, `client` o `transport`. Una pérdida de red normalizada por el SDK no se presenta como un rechazo del servidor. |
| `code` | Identificador estable de causa pública. Nunca una frase, tipo de excepción Go/Dart/JS o código HTTP usado como sustituto de causa. |
| `category` | Clasificación común para políticas de presentación/diagnóstico: validación, autenticación, autorización, conflicto, disponibilidad, etc. No concede permisos. |
| `outcome` | Lo que se sabe sobre la operación, según la sección siguiente. Impide equiparar timeout con rechazo confirmado. |
| `message` | Clave traducible y argumentos públicos tipados. No se programa comparando frases traducidas. |
| `recovery` | Acción semántica de recuperación y `retryAfterMs` opcional. El SDK combina esa indicación con el estado local y la idempotencia. |
| `violations` | Lista de campos inválidos: `pointer` JSON Pointer, código y mensaje estructurado. Vacía si no corresponde. No devuelve el valor recibido. |
El esquema permite códigos futuros con el mismo formato; un código desconocido recibe mensaje genérico y conserva categoría/resultado válidos para diagnóstico. Una clave de traducción desconocida no rompe la pantalla. Una categoría, acción o versión desconocida requiere fallback seguro de protocolo, sin ejecutar automáticamente una recuperación nueva. Los escritores solo emiten campos publicados; los lectores pueden ignorar extensiones opcionales desconocidas. No interpretan esos campos como permisos ni almacenan/loguean todo el objeto recibido.
## 3. Resultado incierto y recuperación
| `outcome` | Significado |
| --- | --- |
| `not-applicable` | Lectura, negociación o fallo sin una mutación de dominio asociada. |
| `not-applied` | Existe evidencia de que este intento no aplicó la operación. No garantiza que otro envío del mismo comando esté sin resolver. |
| `rejected` | Rechazo de negocio definitivo registrado en un recibo del servidor. Reenviar el mismo UUID devuelve ese recibo; no vuelve a ejecutar. |
| `unknown` | No se puede demostrar aplicación o rechazo. Puede haber ocurrido un commit. |
`unknown` exige `recovery.action: resolve-pending` y, en un sobre, referencia propia de comando u operación. El cliente consulta el recibo/estado pertinente y, cuando corresponda, reenvía **el mismo sobre**. No crea otro UUID, cambia precondición ni anuncia fracaso definitivo para desbloquear una acción duplicada. Un fallo local de decodificación después de enviar una mutación también conserva incertidumbre. Autenticación usa su registro de intento específico; no se inventa un recibo de juego para recuperar credenciales.
Las acciones publicadas son `none`, `correct-input`, `authenticate`, `complete-requirements`, `resync`, `resolve-pending`, `retry-same-request`, `change-selection`, `update-client` y `contact-support`. `retry-same-request` solo se utiliza con evidencia y una operación reintentable/idempotente. `rejected` no autoriza ese reintento automático. `retryAfterMs` es un entero no negativo, máximo 24 h en este perfil, y solo acompaña reintento/consulta pendiente; cuotas superiores necesitan otra política/versionado.
Una nueva intención después de rechazo/revisión obsoleta exige oferta actual y decisión del usuario. Los errores no contienen comandos ejecutables, destinos arbitrarios ni nuevas credenciales. Una recomendación de recuperación no elude condiciones, protección, membresía o generación de mando.
## 4. HTTP, mensajes y recibos
HTTP utiliza [Problem Details, RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html), con `error` y `correlation` como extensiones propias:
```json
{
"type": "urn:juegoland:problem:room-full",
"title": "La sala está completa",
"status": 409,
"correlation": {
"requestId": "f8aa2fa1-aec8-41c8-a27b-644e999df03a",
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9"
},
"error": {
"modelVersion": 1,
"origin": "server",
"code": "ROOM_FULL",
"category": "conflict",
"outcome": "rejected",
"message": { "key": "errors.roomFull", "args": {} },
"recovery": { "action": "change-selection" },
"violations": []
}
}
```
Se emite `application/problem+json`, con `status` igual al HTTP real, caché `no-store` y título público localizado cuando proceda. `type` se deriva de `code` como `urn:juegoland:problem:` más código en minúsculas y guiones; cada código necesita documentación antes de publicarse. La URN identifica el tipo y no se descarga desde el cliente. `title` es fallback humano, sin ser la autoridad de causa; los SDK usan el mismo `error`.
Los mensajes v2 `platform.error`/`game.error` incorporan `ErrorMessagePayload { correlation, error }` dentro de su sobre negociado. No llevan un estado HTTP ficticio. Los rechazos de negocio incorporan el mismo `error` en su recibo, conservando recurso/revisiones/UUID según el contrato completo. `CommandRejection` valida solo ese fragmento; no constituye un recibo completo de sala o juego. Un rechazo necesita origen servidor y resultado `rejected`.
El error duradero de un recibo no cambia al reintentarlo ni al cambiar idioma/dispositivo. `Correlation` pertenece a la entrega: `requestId` de intento/traza, `commandId` estable, `operationId` o `subscriptionId` cuando corresponda y esté autorizado. No se guarda una nueva traza dentro del recibo para modificarlo en cada consulta. Si la recepción ya identifica el comando, un adaptador no elimina esa correlación al normalizar el fallo. Un ID de soporte no expone stack, hosts, SQL o estado privado.
Los [fixtures comunes](../../contracts/shared/fixtures.json) contienen el mismo núcleo `ROOM_FULL` en HTTP, mensaje y rechazo; el verificador compara igualdad. La nueva estructura reemplaza los errores planos del **borrador** de plataforma al adoptar este contrato. El [protocolo de juego v1](../game-engine/format-and-protocol.md) conserva sus sobres/códigos; cualquier puente normaliza al modelo común en el SDK sin modificar silenciosamente v1.
## 5. Códigos, localización y privacidad
Cada código publicado registra significado, categoría, clave/argumentos, estados HTTP admitidos y recuperaciones válidas en su contrato de dominio. No se renombra o reutiliza para otra causa. Las [categorías de API](api-and-events.md#8-errores-y-compatibilidad) siguen siendo referencia de plataforma; los juegos registran sus causas al publicar el paquete. Los códigos son extensibles, pero los argumentos/recovery deben estar documentados y probados.
| Ejemplo | Categoría / recuperación |
| --- | --- |
| `INVALID_REQUEST` | `validation`; corregir entrada; violations solo de campos públicos del formulario. |
| `SESSION_EXPIRED` | `authentication`; reautenticar y eliminar vistas privadas. |
| `FORBIDDEN` | `authorization`; mensaje genérico sin identificar a terceros. |
| `ROOM_FULL` | `conflict`; cambiar selección; rechazo confirmado si ya tiene recibo. |
| `STALE_CONTROL_GENERATION` | `conflict`; recuperar recibo/operación y sincronizar antes de una nueva intención. |
| `RATE_LIMITED` | `rate-limit`; demora acotada; distinguir intento no aplicado de una intención anterior pendiente. |
| `CONNECTION_LOST` | `transport`; resolver pendiente si se envió una mutación; lectura reintentable si no la hubo. |
| `INVALID_RESPONSE` | `protocol`; fallback local; conservar incertidumbre de una mutación enviada. |
Web y Flutter traducen las mismas claves/argumentos revisados de `es`/`en`; idioma de interfaz no cambia código, resultado ni recuperación. No se presentan mensajes crudos de excepciones. Las causas internas permanecen en diagnóstico restringido, fuera del DTO. No se incluyen teléfono/OTP, prueba de edad, tokens, datos de otra plaza ni payload rechazado. También `violations` evita enumerar recursos ajenos; un fallo de secreto/credencial conserva el mensaje genérico de su política.
Los fallos de proxy, HTML inesperado, bytes inválidos, cortes y timeout se normalizan localmente con esta estructura, manteniendo `origin` y el resultado que realmente se conoce. El SDK no inventa un `requestId` servidor ni una causa de negocio a partir de un 502 genérico.
## 6. Generación, conformidad y adopción
```powershell
npm run contracts:generate
npm run contracts:check
go test ./contracts/shared/go
dart contracts/shared/dart/verify.dart
dart analyze contracts/shared/dart
```
El generador y el verificador funcionan desde la raíz. `contracts:check` detecta si un DTO generado se separó del esquema y verifica ejemplos positivos/negativos. El esquema es la fuente de estructura; las relaciones semánticas de las secciones 3/4 se comprueban también en [validate.ts](../../contracts/shared/validate.ts) y en los fixtures compartidos. Esas relaciones no se deducen solo de los tipos generados. El perfil del generador cubre únicamente las construcciones usadas en esta fuente; ampliarlo exige fixtures y revisión de las tres salidas. No es un generador general de OpenAPI ni del SDK de reglas.
Los bindings Go/Dart son DTOs estructurales: sus tipos/decodificación **no sustituyen** rangos, enums, relaciones, límites ni autorización. La validación de esquema/semántica se verifica en las herramientas TypeScript; Go/Dart comprueban lectura/serialización de los mismos ejemplos válidos. Antes de usar los adaptadores en red deben superar también los casos negativos con validadores propios conformes. La recepción textual necesita además límites de bytes/profundidad, rechazo de claves duplicadas y Unicode inválido antes de parsear; JSON Schema sobre un objeto no detecta claves duplicadas.
Adopción por cortes: usar los tipos compartidos para el catálogo; integrar `ErrorData` en adaptadores HTTP y errores locales web; después binding/recibos v2 y futura app con su validador. Una capacidad sigue desactivada hasta tener su camino completo verificado. Las entidades privadas nunca se serializan directamente por haber generado una clase pública.
Comprobación inicial: 22 modelos generados sin deriva, 14 fixtures válidos y 25 rechazos de esquema/semántica; núcleo de error idéntico entre HTTP, mensaje y recibo. Go y Dart conservan los 14 fixtures al decodificar/serializar; análisis Dart y pruebas de API Go pasan. TypeScript del frontend y Svelte pasan; para Svelte se usó temporalmente configuración por defecto equivalente a las opciones de compilador actuales, porque cargar Vite intentaba escribir una caché bloqueada por el entorno. No se hizo un ensayo de binding WSS ni de una app Flutter.

Powered by TurnKey Linux.