Diseñé un schema escalable y auditable para los mundos de mis libros. Nunca tuve que cargar un libro completo de una sola vez
Cómo diseñé el formato de packs de Verso — JSON auditable en vez de una base de datos, y un gate de spoilers que se resuelve comparando dos enteros — para que crezca capítulo a capítulo sin arriesgar nada de lo ya leído. Con pack de Drácula descargable de referencia.
Verso (la app de la que hablé en el primer post de esta serie — github.com/andyeswong/verso) no sabe nada de ningún libro. Todo lo que ves cuando abres una lectura — las escenas, los personajes, cuándo se revelan — vive afuera del código, en un pack. La app es el motor; el pack es los datos.
Pude haberlo guardado en un .sqlite ya armado (el formato de una base de
datos SQLite — una base de datos completa metida en un solo archivo
binario, sin servidor —
sqlite.org): más rápido de leer, más
compacto. No lo hice. Un binario no se puede versionar en git (el sistema
que registra el historial de cambios de un proyecto de código —
git-scm.com) de forma legible, no se puede editar
con un editor de texto, y sobre todo — no se puede revisar el diff (la
comparación línea por línea entre dos versiones de un archivo). Con JSON
(un formato de texto simple para guardar datos estructurados — el que
probablemente ya has visto si alguna vez abriste la respuesta de una app
web — json.org/json-es.html), cuando
corrijo una entidad, veo exactamente qué cambió. Con un binario, veo que
cambió algo.
De qué está hecho un pack
Un pack es una carpeta con esta forma:
mi-pack/
pack.json manifiesto: id, versión, tema visual
series.json la saga y sus libros
books/<libro>/
book.json las partes del libro
units.json las ~87 unidades (capítulos, preludio, interludios...)
entities.json el grafo del mundo: personajes, lugares, objetos
cards.json qué tarjeta se pinta en cada unidad
media/
kaladin-canon.webp
ch01-carromato.webp
...
unit y no chapter, porque un libro trae más que capítulos — preludio,
prólogo, interludios, epílogo — y todos comparten el mismo eje de lectura.
Y los archivos van separados por tipo a propósito: entities.json lo edito
todo el tiempo; units.json casi nunca. Un solo archivo gigante hace
ilegible cada cambio pequeño.
La regla que hace posible el gate de spoilers
Cada pieza revelable —una unidad, una entidad, una tarjeta— lleva un
reveal_key: un entero que se calcula así:
reveal_key = número_de_libro * 100000 + orden_de_lectura_de_la_unidad
Es lo que convierte el gate de spoilers en una comparación de enteros, sin
cruces de tablas: si el reveal_key de algo es mayor que el de tu
progreso, esa cosa no se muestra — se cuenta ("3 más sin revelar"), pero no
se nombra.
De ahí sale la regla más importante al escribir un pack, y la que más fácil se rompe: el punto de revelación de una entidad es su primera aparición, menciones incluidas. Si el capítulo 1 nombra a un personaje aunque sea de pasada, ese personaje ya se reveló — ponerlo más tarde crea apariciones que el gate nunca podrá mostrar, porque el texto ya lo delató antes de que la ficha se desbloqueara.
Por qué las imágenes cuelgan de entidades, no de capítulos
Un personaje tiene una imagen canónica, reusada cada vez que aparece.
Colgar la imagen del capítulo en vez de la entidad produce cuarenta
Kaladines distintos a lo largo del libro — literalmente pasó en una de las
primeras versiones. La imagen vive en entities.json, apuntando a un
archivo en media/, y cards.json la referencia por id cuando toca
mostrarla.
Escalable y modular: crece contigo, no de golpe
El schema nunca me pidió tener el libro completo antes de poder usarlo. Lo construí capítulo a capítulo, al mismo ritmo en que iba leyendo — porque armar un pack a mano lleva tiempo, y esperar a terminar El camino de los reyes (cerca de mil páginas) antes de poder abrir la app habría sido absurdo.
La primera versión de mi pack de Sanderson cubría del preludio al capítulo 14: 19 unidades, 63 entidades, 82 imágenes. Cuando seguí leyendo, extendí el mismo pack hasta el capítulo 24 — no reescribí nada de lo anterior, solo agregué: 29 unidades, 84 entidades, 113 imágenes. El índice de la app pasó a mostrar hasta el capítulo 24, y todo lo que va del 15 en adelante apareció sellado — sin título, contado pero no nombrado — porque mi progreso seguía marcado en el 14.
Eso funciona por dos decisiones del schema, no por casualidad:
- Actualizar un pack nunca toca tu progreso, tus notas ni tu media propia. Esas tres cosas viven en tablas separadas del pack — la regla es que el estado del usuario nunca puede depender de lo que traiga el contenido. Agrandar el pack no reinicia nada de lo que ya tenías.
- Emparejar por id, no por posición. Si una unidad desaparece o cambia de orden en una versión nueva del pack, tu progreso baja a la unidad existente más cercana — nunca sube. No hay forma de que una actualización te adelante capítulos que no has leído.
El mismo principio hace el schema modular en otro sentido: como los
archivos van separados por tipo (entities.json, units.json,
cards.json...), extender solo toca los archivos que cambian. Agregar diez
capítulos nuevos no reescribe el grafo de entidades entero — solo le suma
las piezas nuevas al final. Y como todo es JSON, cada extensión es un diff
que se puede revisar antes de importarla: auditable no es solo "se
puede leer el cambio", es "se puede leer el cambio de un pack que ya está
en producción, sin arriesgar el que ya tenías cargado".
Un bug real, del pack de Drácula
Armando el pack de referencia de esta serie, una entidad —Carfax, la casa
donde se instala el conde— se quedó sin ilustración durante toda una
pasada de trabajo. La causa no fue que el campo canon_asset (la imagen
de la entidad) estuviera vacío. El campo directamente no existía.
En muchos lenguajes, leer un campo que no existe y leer uno que vale
null se ven idénticos si usas el método equivocado — los dos devuelven
"nada". Son cosas distintas: una entidad sin imagen asignada todavía es un
estado válido del pack; una entidad sin el campo siquiera declarado es un
dato incompleto. El arreglo fue generar la imagen que faltaba y declarar
el campo — y, aparte, revisar que el resto del pack no tuviera la misma
ambigüedad escondida en otro lado.
Verlo armado de verdad: el pack de Drácula
El repo trae un pack de referencia completo — Drácula, de Bram Stoker, 1897, dominio público, así que se puede repartir sin tocar los derechos de nadie. 12 escenas y 10 entidades con imagen propia (23 ilustraciones en total), en grabado al acero — la técnica de las ediciones ilustradas de la época del libro, no un "gótico oscuro con IA" genérico.
Descargar el pack de referencia (9.8 MB) — o revisa primero la página de la release, que trae el detalle de qué cambió.
Para instalarlo: descomprime el zip y, dentro de la app, en ajustes →
importar pack, apunta a la carpeta dracula-es. Si ya tenías una
versión anterior instalada, no hace falta desinstalar nada — el
importador siempre vuelve a bajar la media y reemplaza el pack entero, así
que una actualización se resuelve sola.
El pack se audita sin salir de él
Cada asset del schema tiene tres campos opcionales para esto: tool,
prompt y seed — con qué modelo se generó, con qué texto exacto, y con
qué semilla, si se fijó una. En la versión final del pack de referencia los
23 vienen completos: abres assets.json y ves, pegado a cada imagen, que
se hizo con Seedream 5.0 y el prompt entero que la produjo — no una nota
general en un README diciendo "hecho con IA", sino la procedencia exacta
de cada pieza, ahí donde vive la pieza.
El campo seed se quedó vacío a propósito, en las 23. No se fijó ninguna
semilla al generarlas, así que declarar una habría sido inventar un dato
que no existe — el mismo error, en miniatura, que el bug de Carfax de más
arriba. Y ese vacío dice algo real: estas 23 imágenes no son reproducibles
bit a bit. Que un pack pueda declarar eso de sí mismo —qué no se puede
hacer con él, no solo qué sí— es la parte más honesta de auditarlo.
Lo que dejo abierto
El pack de Drácula que se puede descargar hoy es exactamente el mismo formato que uso para mi propio pack de El camino de los reyes — solo que ese no se publica, porque incluye citas textuales de Sanderson e ilustraciones de sus personajes, y el motor está pensado justo para que eso no haga falta: el código no sabe qué es Roshar, y el pack de cada quien vive en su propio dispositivo, no en el repo.
Con esto cierro la serie: la app que resuelve cómo leo, cómo se arma el material que la alimenta, y de dónde salieron las imágenes.
artículos_relacionados
Cada vez que le pedía a ChatGPT que me mostrara el mundo del libro, perdía lo que había generado el día anterior
Construí Verso, un lector de worldbuilding que corre al lado del libro que estoy leyendo, porque ChatGPT perdía lo que generaba y no respetaba mi progreso de lectura.
Agentes de voz: la voz es lo que menos importa
Le metimos voz en tiempo real al moderador de Quorum. El bug que casi tira el proyecto no fue de audio ni de latencia — fue de memoria, protocolo e idioma. El camino completo, con lo que sirvió y lo que no.
Cuando mis directores no se ponen de acuerdo, no meto una IA que decida. Meto cuatro que los obliguen a decidir
Construí Quorum: una sala de deliberación con IA que fuerza a mi equipo — y a mí — a completar el razonamiento antes de decidir, con una regla dura: nada entra a la memoria de la empresa sin que un humano lo apruebe.
posts_recientes
Cada vez que le pedía a ChatGPT que me mostrara el mundo del libro, perdía lo que había generado el día anterior
1 sept 2026
Agentes de voz: la voz es lo que menos importa
31 ago 2026
Cuando mis directores no se ponen de acuerdo, no meto una IA que decida. Meto cuatro que los obliguen a decidir
31 ago 2026
Me secuestraron un servidor, y la puerta la dejé abierta yo
20 ago 2026