artículos / Diseñé un schema escalable y auditable p...

Diseñé un schema escalable y auditable para los mundos de mis libros. Nunca tuve que cargar un libro completo de una sola vez

AWONG 5 minutos de lectura 4 vistas

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.

Diseñé un schema escalable y auditable para los mundos de mis libros. Nunca tuve que cargar un libro completo de una sola vez

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

posts_recientes