# Modelo de datos
Diseño de la migración del contenido a PostgreSQL. **Fase 1 —este documento—
está entregada y pendiente de verificación**; no hay código escrito todavía.
Versión presentable, con diagramas entidad-relación:
< https: / / claude . ai / code / artifact / d87ceb4d-6038-402f-852e-13ff5d86f02f >
Este archivo es la especificación; el artefacto es la misma información puesta
para leerse. Cuando se implemente, la verdad pasa a ser
`src/lib/server/db/schema.ts` y esto queda como el porqué.
## Por qué
Lo que hay hoy en `src/content/**` son registros disfrazados de documento: una
canción tiene veinte campos estructurados en YAML —`registro`, `duracion` ,
`interpretes` , `versiones` , `videos` , `etiquetas` — y la letra entera también va
en el frontmatter. El cuerpo en Markdown son cuatro líneas de comentario.
Dos hallazgos concretos, no cuestión de gusto:
**1. Alguien puede perder una canción que pagó.** Cinco tablas apuntan a una
canción por una cadena de texto —`carrito_item`, `pedido_item` , `compra` ,
`favorito` , `valoracion` — y ninguna tiene clave foránea, porque el destino no
está en la base sino en un archivo. Renombrar un slug deja huérfana una compra
sin que salte ningún error en ningún sitio.
**2. `estilo` hace dos trabajos distintos.** En el catálogo son las secciones del
artista (baladas románticas, clásica, rock, pop melódico); en el taller, una
«ficha de estilo» es un género hispanoamericano (bolero, tango, cumbia). Son dos
entidades que comparten palabra y hoy comparten nombre en el código. La
clasificación por país es del género, no del estilo, y mientras sean lo mismo no
se puede ni escribir.
A eso se suma que nada se valida —`estilo: bolreo` no falla, la canción
desaparece de su sección—, nada se consulta sin cargar el catálogo entero en
memoria, y cambiar una coma exige reconstruir y desplegar.
Alcance: **57 tablas** (13 ya existen), **48 archivos** que migrar, **22 rutas**
que hoy leen `$lib/content` .
## Decisiones transversales
| Decisión | Elegido | Motivo |
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Identificadores | `text` (nanoid 21), como las tablas actuales | El slug es texto que se lee y algún día se corrige; una clave primaria no debería cambiar nunca |
| Slugs | únicos + tabla `redireccion` | Renombrar deja de romper enlaces publicados; la dirección vieja responde 301 |
| Prosa | columna `text` con Markdown, compuesto en el servidor | Comprobado: ningún archivo de contenido usa sintaxis de Svelte, así que no se pierde nada al dejar de compilarlo con mdsvex |
| Listas | tabla si se consulta, `text[]` si solo se enseña | Las etiquetas cruzan el catálogo; los temas y rasgos de un género solo se pintan |
| Publicación | `estado` (borrador · publicado · retirado) | Escribir una ficha a medias no puede significar publicarla |
| Fechas | `timestamptz` siempre; `creado_en` y `actualizado_en` | Y en los legales, además, historial: hay que poder demostrar qué decía el día de una compra |
| Enumerados | texto con `CHECK` , no `ENUM` de Postgres | Es lo que genera Drizzle y permite añadir un valor sin una migración que bloquee la tabla |
| Borrados | reales en contenido; `retirado` en lo vendido | Una canción comprada no se borra nunca: la compra tiene que seguir apuntando a algo |
## Las entidades
### 1 · Catálogo
- **`estilo`** _(cambia)_ — id, slug, nombre, resumen, orden, `principal` con
índice parcial único (hoy es un acuerdo de palabra), imagen_id, estado.
- **`album`** _(cambia)_ — + numero_catalogo, `formatos text[]` ,
pais_edicion_id. Los campos de `edicion` dejan de estar anidados.
- **`cancion`** _(cambia)_ — album_id nulo en los singles; **el álbum «Singles»
deja de existir como registro**, era un invento para pintar y se resuelve al
consultar. letra, nota, inspiracion y traducción salen del YAML.
- **`cancion.version_de_id`** _(decidido)_ — **una versión es una fila de
`cancion` , no una tabla aparte.** Nulo en las principales. Se llegó aquí
descartando partir en `obra` y `grabacion` : en ese reparto el estilo y las
etiquetas colgarían de la obra, y una versión no podría tener los suyos —una
balada con arreglo de bolero no cabría—. Con una fila por versión, cualquier
campo puede cambiar.
De regalo se arregla algo que estaba mal: **el disco pasa a ser de la fila** .
Hoy `album_id` y `numero` cuelgan del tema, así que la «Maqueta 2018» es el
corte 1 del álbum igual que la versión de estudio, y es falso.
Cuatro cabos que hay que atar, o se vuelve en contra:
1. **Vista `cancion_principal`.** Cuatro funciones devuelven listas de
canciones y siete rutas las pintan, más los recuentos por estilo y por
etiqueta. Si a una se le olvida `version_de_id IS NULL` , el catálogo dice
veinticinco temas donde hay dieciocho y nadie lo nota. Lo normal es
consultar la vista; pedir versiones es lo que hay que escribir a propósito.
2. **Un solo nivel, comprobado en la base.** Una autorreferencia admite ciclos
—A apunta a B y B a A— y una consulta recursiva sobre eso se cuelga. Se
ataja con una clave foránea compuesta contra una columna generada
`es_version` , sin triggers.
3. ** `ala_venta` solo en las principales**, y comprar la principal entrega
todas sus versiones. Es la regla que ya estaba escrita, ahora literal.
4. **Los campos se copian al crear, no se heredan al leer.** Un nulo heredado
nunca se sabe si es «no lo sé» o «lo mismo de arriba». Y aquí copiar es
además lo correcto: la maqueta de 2018 no tiene la misma letra, le falta el
puente.
- **`persona`** _(nueva)_ — nombre, nombre_legal, ipi. Hoy autores e intérpretes
son cadenas sueltas repetidas en cada archivo.
- **`cancion_persona`** _(nueva)_ — rol (letra · musica · interprete · arreglos ·
produccion), orden.
- ~~**`version`**~~ — **desaparece como tabla** : cada versión es una fila de
`cancion` con `version_de_id` . El `isrc` va en esa fila, no en la principal:
identifica una grabación, y cada arreglo es una grabación distinta. El `iswc`
es de la composición y se repite en la familia.
- **`registro_obra`** _(cambia)_ — iswc, entidad, numero, editorial. 1:1 con la
canción.
- **`autoria`** _(cambia)_ — porcentaje `numeric(5,2)` , no entero: los repartos
reales llevan decimales. Suma 100 comprobado en la base.
- **`etiqueta`, `cancion_etiqueta` ** _(cambia)_ — hoy se deducen de texto libre,
así que «desamor» y «Desamor» son dos etiquetas y nadie lo ve.
- **`traduccion`** _(nueva)_ — cancion_id + idioma. Hoy solo cabe una.
- **`video`, `cancion_enlace` , `album_enlace` , `credito_album` ** _(cambia)_ —
**dos tablas de enlaces y no una polimórfica** : una clave foránea de verdad
vale más que ahorrarse una tabla.
### 2 · Taller, géneros y glosario
**Decidido: el catálogo de géneros aspira a ser completo** y se llena poco a
poco. Eso cambia el peso de esta área: la ficha de género deja de ser una
tarjeta y pasa a ser la pieza más rica del sitio, con su historia, sus
instrumentos, sus letristas y sus obras de referencia.
- **`pais`** _(nueva)_ — código ISO 3166-1, nombre, gentilicio, region.
- **`genero`** _(nueva)_ — bolero, tango, cumbia. En columnas solo lo suyo:
origen_lugar, origen_ano (ordenable, hoy no lo es), tempo, rima, y las notas
en prosa `metrica_nota` y `compas_nota` para el matiz que no cabe en una
tabla —«a menudo alternados» es lo que hace útil la ficha—.
**Lo comparable sale a tablas auxiliares.** Con ocho fichas se lee; con cien,
que es a donde va esto, hay que poder preguntar.
- **`genero_origen`** _(nueva)_ — genero_id, origen_id, **tipo** (deriva ·
fusiona · influye), `principal` , nota, orden.
**Sustituye a `padre_id`, que no aguantaba.** Un género no deriva de uno:
el bolero-son es bolero _y_ son; la salsa viene del son, el mambo y la
guaracha; la bachata, del bolero, el son y el merengue. Con un solo padre, la
primera fusión obliga a elegir cuál de los dos mentir —y `bolero` y
`son-cubano` ya existen como fichas separadas—.
El tercer tipo, `influye` , es más débil que derivar y hoy ya está escrito,
aunque en el campo equivocado. Esto es literal de `son-cubano.md` :
> metrica: 'Octosílabo, **herencia de la décima y la copla españolas**'
Un vínculo entre géneros, metido dentro del campo de la métrica porque no
había dónde ponerlo.
El grafo guarda la verdad y la arista `principal` —única por género, con
índice parcial— da el árbol de navegación. Los subgéneros son las filas que
apuntan a uno con tipo `deriva` .
Guardas: `genero_id <> origen_id` , y **un disparador contra los ciclos** . Un
grafo admite que A derive de B y B de A, y una consulta recursiva sobre eso no
termina. En una tabla de este tamaño, comprobarlo al insertar con un
`WITH RECURSIVE` no cuesta nada.
- **`metro`** + ** `genero_metro` ** _(nuevas)_ — codigo, nombre, `silabas` , y
`termino_id` contra el glosario, porque «octosílabo» es también una entrada
suya. La tabla puente lleva `predominante` .
Semilla: de pentasílabo a alejandrino, más una fila ** `libre` ** con `silabas`
nulo, para que «géneros sin métrica fija» —la bachata: «Libre, cercana al
habla»— también sea una consulta y no una ausencia.
El motivo, con los datos delante: el octosílabo aparece en el bolero, el son y
el tango; el endecasílabo en el bolero y el tango; el alejandrino en el tango.
Los tres escritos en prosa. ** «Enséñame los géneros en octosílabo» es justo lo
que un taller de métrica necesita poder preguntar**, y hoy no se puede.
- **`compas`** + ** `genero_compas` ** _(nuevas)_ — codigo ('2/4', '4/4', '3/4',
'6/8', '2/2'), nombre, numerador y denominador —guardados aparte para poder
pedir los binarios o los ternarios—, y `principal` en la puente. El bolero
lleva dos filas: 2/4 en el clásico, 4/4 en el moderno.
- **`genero_alias`** _(nueva)_ — genero_id, nombre, pais_codigo, nota.
_Son cubano_ , _son jarocho_ , _son huasteco_ , _son montuno_ . La cumbia
colombiana y la argentina. Con cien fichas, buscar «son» devuelve cuatro cosas
que no se parecen; cada una tiene que encontrarse por como la llaman donde se
toca.
- **`genero_seccion`** _(nueva)_ — clave, titulo, cuerpo, orden. **Toda la prosa
de la ficha**: introducción, historia, estructura de las canciones, motivación
y temática. Y lo que surja mañana. Con una columna por bloque, en un mes hay
quince columnas y la mitad vacías; siendo secciones ordenadas, un apartado
nuevo no cuesta una migración. Es la respuesta a «y otros campos que puedan
surgir».
- **`genero_pais`** _(nueva)_ — relacion: cuna · adoptado. La cumbia nace
colombiana y se hace mexicana, peruana y argentina; el tango es de las dos
orillas. Un solo país sería mentira.
- **`instrumento`** _(nueva)_ — slug, nombre, **familia** (cuerda · viento ·
percusión · teclado · electrófono · voz) y subfamilia libre, pais_codigo,
descripcion, **afinacion** , `imagen_id` y `muestra_id` contra `medio` —el
sonido de muestra es un archivo como cualquier otro—, estado.
- **`genero_instrumento`** _(nueva)_ — con **importancia** (esencial · habitual ·
ocasional) y **papel** («marca la clave», «lleva el bajo»). Sin eso, el tres
cubano y una trompeta ocasional pesarían igual en la ficha, y no es verdad.
- **`genero_persona`** _(nueva)_ — papel: letrista · compositor · interprete ·
fundador. **No hay tabla `letrista`** : un letrista es una persona, y con dos
tablas la misma acabaría existiendo dos veces con distinta grafía el día que
colabore con alguien que además es figura de un género. `persona` gana los
campos biográficos: nacimiento, defuncion, pais_codigo, retrato_id, semblanza.
- **`obra_referencia`** + ** `obra_referencia_persona` ** _(nueva)_ — titulo, ano,
genero_id, nota, enlace, `cita` . ** «Letras de referencia de un letrista» y
«canciones del género» son la misma entidad**, mirada desde un lado o desde el
otro.
⚠️ **No se alojan letras completas de terceros.** «Bésame mucho» tiene dueño y
lo tendrá durante décadas. Se cita título, autor, año y un fragmento breve al
amparo del derecho de cita —el mismo que invoca nuestro propio aviso legal— y
se enlaza a dónde escucharla. Por eso el campo se llama `cita` y es corto, no
`letra` .
- **`cancion_genero`** _(nueva)_ — un tema propio escrito en ese género, aunque
en el catálogo viva bajo «baladas románticas». Es lo que permite que una ficha
enseñe obra propia además de la ajena.
- **`capitulo`** _(cambia)_ — tipo (principio · ficha), genero_id solo en las
fichas, cuerpo, estado.
#### Prompts para IA generativa
Biblioteca de prompts por género para Suno y equivalentes.
- **`modelo_ia`** _(nueva)_ — codigo, familia (suno · udio · riffusion ·
stable-audio · mureka), **version** , nombre, vigente, nota_sintaxis y
** `esquema jsonb` **: un JSON Schema que describe la forma que toman los
prompts de ese modelo. Con él se validan al guardar y el panel pinta el
formulario solo, sin una pantalla por cada IA.
**El prompt depende del modelo, no de la IA.** Un prompt de Suno v3 no es uno
de v4: cambian la sintaxis, las etiquetas y lo que el modelo ignora. Si la
clave foránea apunta a «Suno» a secas, el día que salga la versión siguiente
queda una biblioteca de prompts sin saber para qué servían.
- **`animo`** _(nueva)_ — codigo, nombre, **familia GEMS** , valencia, energia,
descripcion.
La codificación es la **GEMS** (_Geneva Emotional Music Scale_), hecha para
emoción musical y no para emoción en general: asombro, trascendencia, ternura,
**nostalgia** , serenidad, fuerza, alegría vital, tensión y tristeza. Más los
dos ejes del modelo circumplejo de Russell —valencia y energía—, que sirven
para ordenar e interpolar y que además son redactables: «alta energía,
valencia baja» es una instrucción; «tensión» a secas, no.
Se llama `animo` y no `version` **a propósito** : `version` ya significa otra
cosa en este modelo —`cancion.version_de_id`— y con las dos compartiendo
nombre, en un mes ninguna consulta se lee sola.
- **`prompt`** _(nueva)_ — modelo_ia_id, genero_id, animo_id, idioma, notas,
`valoracion` 1– 5, `ejemplo_id` contra `medio` , `preferido` , visibilidad
(privado · taller · publico), estado, y ** `contenido jsonb` ** con el prompt
propiamente dicho.
**El contenido va abierto, y aquí sí toca.** La forma del prompt la define un
tercero y cambia sin avisarnos: Suno separa estilo de letra y admite
exclusiones, otro modelo pedirá `weirdness` o `style_influence` . Modelar eso
en columnas es perseguir el esquema de otro. Es el caso contrario al del
contenido del sitio, donde el esquema es nuestro y por eso ahí sí se modela.
Dos condiciones para que no se pudra, que es lo que le pasa siempre a un campo
abierto:
1. **Se valida contra el `esquema` de su modelo.** Sin eso, en seis meses hay
prompts con `exclude` , `excludes` y `excluded` y ninguna forma de saber
cuál era el bueno. Abierto no es lo mismo que sin validar.
2. **Lo que se consulta se queda fuera del JSON.** Género, ánimo, modelo,
valoración e idioma son columnas. Postgres indexa `jsonb` con GIN, así que
no sería fatal, pero «los prompts que funcionaron para el bolero» tiene que
seguir siendo una consulta trivial.
`jsonb` y no `json` : el binario se indexa y normaliza las claves; el otro
guarda el texto tal cual y no sirve para buscar.
**La versión del modelo no se repite dentro.** Vive en `modelo_ia.version` ; si
además se escribe en el JSON, tarde o temprano dirán cosas distintas. Si una
API la exige en el payload, se compone al enviar.
Se guarda **si funcionó** —valoración y archivo con el resultado—, porque sin
eso se acumulan doscientos prompts sin saber cuáles servían. Varios por
combinación, con uno preferido: esto se itera, no se acierta a la primera. Y
el idioma importa: en inglés suelen funcionar mejor, incluso para música en
español.
- **`termino`** _(nueva)_ — el glosario. termino único, categoria, `definicion`
de una o dos frases (lo que cabe en un globo sin taparle el texto a nadie),
ampliacion, ejemplo.
- **`termino_variante`** _(nueva)_ — «sinalefas», «heptasílabos». Sin esto el
marcado solo acierta en singular, que es casi nunca.
- **`capitulo_termino`** _(nueva)_ — se calcula **al guardar el capítulo, no al
pintarlo**; y siendo tabla, se puede desmarcar una aparición cuando la palabra
esté usada en otro sentido.
Regla editorial: **solo la primera aparición de cada término por página.** Marcar
las veintidós veces que sale «estribillo» convierte el texto en un campo de minas
subrayado.
### 3 · Editorial
- **`entrada`, `entrada_cancion` , `entrada_etiqueta` ** _(cambia)_ — hoy la lista
de canciones son slugs sin comprobar: si el tema no existe, la entrada enlaza a
un 404 y nadie se entera.
- **`documento_legal`** + ** `version_legal` ** _(nueva)_ — historial con
`vigente_desde` y `resumen_cambios` . No es burocracia: es prueba.
- **`pagina`**, ** `redireccion` ** _(cambia / nueva)_ .
### 4 · Cuentas y comercio
- **`usuario`, `identidad` , `sesion` , `codigo_acceso` , `limite` ** _(sin cambios)_ .
- **`carrito_item`, `pedido_item` , `compra` ** _(cambia)_ — `cancion_slug text`
pasa a `cancion_id fk` . En `pedido_item` se congelan título y precio: una
factura tiene que seguir diciendo lo que decía.
- **`datos_facturacion`** _(nueva)_ — tipo, nombre, nif, dirección,
pais_codigo (decide el IVA), predeterminada.
- **`factura`** _(nueva)_ — serie y numero **correlativos y sin huecos** , base,
iva_tipo, iva_cuota, total, y `datos_congelados jsonb` : si el cliente cambia su
dirección, la factura vieja no puede cambiar con él.
- **`descarga`** _(nueva)_ — para poder demostrar la entrega en una reclamación.
### 5 · Interacción
- **`favorito`, `valoracion` ** _(cambia)_ — clave foránea, y `CHECK` de 1 a 5 en
la base y no solo al validar.
- **`reproduccion`** _(nueva)_ — con `segundos` : una reproducción de tres
segundos no es una escucha, y contarla igual falsea la cifra. Agregación por
día y borrado del detalle a los noventa días **son parte del diseño** , no un
apaño para después.
### 6 · Medios
- **`medio`** _(nueva)_ — clase, clave, ** `publico` ** (los fragmentos sí, el
audio completo nunca; hoy eso lo garantiza la carpeta), mime, bytes,
duracion_s, ancho, alto, sha256, alt. Para que la administración pueda subir
audio, un archivo tiene que ser una fila.
### 7 · Bocetos, no se construyen ahora
`lista` + `lista_cancion` (la playlist que sigue sin definirse), `encargo` ,
`inscripcion_taller` . Se dejan sitio porque aparecer después obliga a migrar lo
ya cargado.
## Qué cambia en la web
1. **Sveltia CMS deja de servir.** Es un editor sobre git. Hasta que exista la
administración propia —decidido: para después—, el contenido se carga por
script y por SQL.
2. **El Markdown se compone en cada petición** , no al construir. Se cachea.
3. **Cada página consulta la base.** A cambio, cambiar una letra deja de exigir
un despliegue.
## Fases
1. **Este documento.** Sin código. ← estamos aquí
2. **Esquema y migración, con el sitio leyendo todavía de los archivos.** Un
script vuelca los 48 archivos y comprueba que lo volcado coincide. Si algo no
cuadra, se ve antes de que dependa nadie de ello.
3. **Las claves foráneas de las compras.** Lo primero que se cambia de verdad,
porque es lo único que hoy puede costarle dinero a alguien.
4. **Las 22 rutas, de archivos a consultas.** Área por área, con las pruebas
existentes de guía.
5. **Lo nuevo:** géneros por país, glosario con globos, introducción larga del
taller.
6. **La administración.**
## Pendiente de respuesta
Seis decisiones de negocio. Cambiarlas después de cargar el contenido cuesta
mucho más que ahora.
1. ~~¿Los géneros del taller aspiran a ser un catálogo completo?~~ **Respondido:
sí, y se irá llenando poco a poco.** De ahí la jerarquía, las secciones y la
ficha ampliada.
2. ¿Quién escribe las fichas que faltan? Es musicología: un compás equivocado
sale publicado con su nombre.
3. ¿El glosario se marca solo o a mano? «Clave» es un término del son y también
una palabra normal.
4. ¿Facturas con numeración correlativa? Si se vende, sí, y la serie no puede
tener huecos ni empezar tarde.
5. ¿Se conservan las escuchas por persona? Cambia la política de privacidad, que
hoy dice que no se hace seguimiento.
6. ¿Dónde viven los archivos: disco del servidor o almacenamiento aparte?
7. ¿La biblioteca de prompts es una herramienta interna o contenido del sitio?
Publicar cómo se fabrica un bolero con IA en una web que vende canciones
escritas por una persona dice algo distinto de lo que dice el resto del
sitio. De la respuesta depende si necesita página, si entra en el mapa y si
la ve el buscador.