Vista generada de dec-0127: Modelo de datos: el fichero es la fuente de verdad, el catálogo es un índice derivado
docs/decisions/dec-0127-modelo-de-datos-fichero-ssot.mdVista generada desde
docs/decisions/dec-0127-modelo-de-datos-fichero-ssot.md. No se edita a mano:bun run docs:genla regenera ybun run docs:checkfalla si difiere. El estado aquí es el del model: si discrepa con otra página, manda el model.
| Campo | Valor |
|---|---|
| Estado | LOCKED |
| Fecha | 2026-10-01 |
| Fichero | docs/decisions/dec-0127-modelo-de-datos-fichero-ssot.md |
Enmienda a: dec-0126, dec-0114, dec-0117
Por qué importa (del frontmatter del ADR):
Fija dónde vive cada dato del modelo de Styx. La metadata de la obra tiene su fuente de verdad en el propio fichero, como documento estructurado canónico (styx.json, JSON canónico RFC 8785) más su proyección a tags estándar y portada. El catálogo guarda un índice derivado, desechable y reconstruible desde los ficheros, que sirve los listados con cero lecturas de fichero. Fija también: metadataHash = BLAKE3 del documento, para detectar cambios y sincronizar barato; ETag = hash en la API, más un feed de cambios por revisión para las cachés de cliente, que son la única copia fuera del servidor; artwork en un almacén direccionado por contenido, servido por el daemon con variantes precalculadas; ids estables derivados del fichero (huella y workUid); estado de usuario por (actorId, profileId) fuera del fichero; overlay del catálogo sólo para lo no escribible; y el plugin de enriquecimiento first-class, activo por defecto y reemplazable. Enmienda dec-0126 P1/§6/§7. Sin este ADR, el catálogo se convertiría en la autoridad de facto, el fichero sería una exportación más, y un índice perdido dejaría la biblioteca, el estado de usuario o las cachés de cliente sin forma de reconciliarse.
Nodos del roadmap que lo citan en refs: track/plugin-seams/w2, track/domain-media/file-ssot, track/media-engine/metadata-embed
Páginas de la documentación que lo citan: Metadatos en el fichero (especificado), Modelo de datos (especificado)
Leído de docs/decisions/dec-0127-modelo-de-datos-fichero-ssot.md, el fichero canónico.
dec-0126. waxin, vía AskUserQuestion:
lockear juntos dec-0126 (mecánica de escritura) y dec-0127 (modelo de datos, metadata SSOT
en el fichero), con respuesta a las siete preguntas de §13. Las respuestas, lo que el lock
elige donde waxin delegó (P2), los criterios que fija (P3, P5) y la identidad por layout y por
tipo (dec-0132 M3) están en Lock (2026-10-02, waxin). Donde el
texto de §1–§14 y el lock difieren, gana el lock. Las enmiendas a dec-0126, dec-0114 y
dec-0117 (§11 y lock) entran en vigor hoy.w10/adr-datos, desde w10/vision). Número reservado
para el modelo de datos en docs/overview/vision-ledger.yml. La implementación cae en
track/domain-media (modelo e ids), track/plugin-seams (enriquecimiento y embed),
track/media-engine (lectura y escritura del documento en el contenedor), el daemon (artwork)
y catalog-svc (índice y feed de cambios). El milestone track/domain-media/file-ssot es un
alta propuesta queued en el model, pendiente de ratificar en el lock de este ADR (sin gate hasta entonces).V-N01, V-DATA-01, V-DATA-02, V-DATA-03, V-DATA-04 y
V-DATA-05. Cubre en parte V-N04 (marcas de intro y créditos como capítulos dentro del
fichero, §4.3; la detección sigue sin nodo) y V-N07/V-N10/V-XFER-02 (toda descarga
generada lleva su documento, §9).r01: los bytes de vídeo no atraviesan JS. Leer o escribir dentro del fichero lo hace el
daemon.r04: SeekableMediaSource. Las fuentes remotas son de lectura.r08: catálogo federado Work → Edition → MediaAsset → SourceBinding.r12/r45: caché por tiers y eviction por coste de regeneración.r17: un servicio por authority. Este ADR no añade servicios.r28: cada capa con un consumidor real.r48 §3.2/§3.3 y r49: precedencia de metadatos, MetadataPatch declarativo y
authority: local-authoritative|derived.r58: asset contracts del Experience Contract, que fijan los tamaños de artwork.dec-0019: canonicalización y seeding lease.dec-0033: plugin SDK tipado.dec-0109: facts en @styx/domain y parser en media-core.dec-0114: scanner por contenido, huella y ledger de escaneo.dec-0117: I1, I3, I5, I7 e I8.dec-0121: ingesta por conduit.dec-0124 (PROPOSED): docs como contrato y registro headless.dec-0125 (PROPOSED): estado por perfil con clave (actorId, profileId).dec-0126 (PROPOSED): cómo se escribe dentro del fichero. Este ADR decide qué es
autoridad y cómo se lee rápido. No toca el cómo se escribe.dec-0126 deja hecha la mecánica de escritura: el plugin proyecta, el daemon escribe in-place
sobre padding con journal o reescribe a staging, y la huella excluye los metadatos. Pero en
§6 declara que "la base de datos sigue siendo la autoridad de lo que sirve la API", y en P1
recomienda "catálogo autoritativo y fichero como proyección". La dirección N1 de waxin es la
contraria. El ledger lo registra como conflicto (V-N01, dec-0126:510).dec-0126 P2 recomienda off por defecto en cada raíz. Waxin quiere el enriquecimiento
"de los de por defecto" (V-DATA-04, conflicto con dec-0126:513).apps/catalog-svc/src/service/db/schema.ts: works,
editions, media_assets, media_indexes e ingest_usage). Los ids son internos y
aleatorios. MediaAsset.path guarda una ruta. No hay estado de usuario: "seguir viendo" es un
mock (packages/mocks/src/continue-watching.ts). dec-0125 fija su clave pero no su dueño
(dec-0125 §13).poster: '' (plan vertical, fila "Artwork", rama
w8/vertical-plan). La P7 de ese plan (quién sirve el artwork, catalog-svc o el daemon) la
responde este ADR.| Referente | Qué hace | Qué tomamos / qué evitamos |
|---|---|---|
beets (música, beet write/beet update) | Base de datos más tags en el fichero. write vuelca la base al fichero y update relee los tags. Se salta los ficheros cuyo mtime no cambió: un cambio externo que no toque el mtime no se ve. | Tomamos los dos verbos (escribir el fichero, releerlo). Evitamos la detección sólo por mtime: además de la tupla barata, comparamos el hash del documento (§5). |
| digiKam (Metadata Settings) | Deja elegir dónde se escribe: sólo en el fichero, fichero y sidecar XMP, o sidecar sólo para ficheros no escribibles (vídeo, RAW). Opción "leer del sidecar e ignorar lo incrustado". | El mismo hueco existe aquí: un fichero no escribible necesita otra casa. La nuestra es el overlay del catálogo (§6.3), no un sidecar, porque waxin descartó los ficheros aparte. |
| Lightroom Classic (advanced metadata actions) | Tres estados por foto: "metadata file needs to be updated", "metadata was changed externally" y error/conflicto cuando cambió en los dos lados. El usuario elige importar o sobrescribir. | Tomamos el modelo de estados casi tal cual (fileState, §6.2). La diferencia es de autoridad: aquí, si sólo cambió el fichero, gana el fichero sin preguntar. |
Navidrome (navidrome scan) | Indexa los tags en su base de datos. El escaneo rápido usa el mtime de las carpetas, el completo lo ignora, y versiones recientes añaden un hash de carpeta. | Dos niveles de detección (tupla barata y hash) y el índice como caché reconstruible con un escaneo completo. |
| Jellyfin / Plex / Kodi | La base de datos es la autoridad. NFO y artwork en carpeta son opcionales (Jellyfin sólo los escribe con el saver activado; ver el plan vertical, §1). | Es lo que N1 rechaza. El NFO queda como importación (dec-0126 §7). |
| XMP en MP4 (XMP Part 3) | Paquete XMP (RDF/XML) en una caja uuid BE7ACFCB-97A9-42E8-9C71-999491E3AFAC. FFmpeg lo extrae de MOV/MP4. | Alternativa considerada para el documento (§12). Se descarta como formato principal por peso y por canonicalización; queda como exportación posible. |
| Matroska Attachments | AttachedFile admite cualquier FileMediaType. | Llevamos el documento como adjunto propio (§4.1). |
HTTP (RFC 9110 ETag/If-None-Match, RFC 8246 immutable) y JSON canónico (RFC 8785, JCS) | Validadores fuertes, recursos inmutables por URL y una serialización JSON determinista. | ETag = hash del documento (§7). Las URLs de artwork llevan el hash, son inmutables (§8), y el hash se calcula sobre JCS. |
Lo que no se afirma: qué campos lee cada reproductor de terceros (VLC, Infuse, AirPlay) de los
tags estándar. Se mide con fixtures en el ticket, como dice dec-0126 §2.3.
styx.json, en JSON canónico RFC 8785. Junto a él
va su proyección a tags estándar y la portada, para que VLC, AirPlay o cualquier
reproductor la vean (§4).metadataHash = BLAKE3-256(JCS(documento)). Se guarda en el índice junto a la tupla
barata de observación. Detecta cambios, decide si hay que releer y es el validador de las
cachés (§5).ETag, y hay un feed de cambios por revisión
monotónica (§7).sha256, el de dec-0126 §3.1), servido
por el daemon con URLs inmutables y variantes precalculadas fuera del camino de servicio.
El documento referencia todo el artwork por hash. Sólo la portada viaja en bytes dentro del
fichero (§8).AssetId de la huella de payload y WorkId de un
workUid que vive en el documento. El estado de usuario y los enlaces sobreviven a una
reconstrucción del índice y a la migración entre servidores (§6.4).(actorId, profileId, workKey|assetKey) (§10).dec-0126: P1 se resuelve hacia fichero-SSOT, y §6 y §7 se sustituyen por
§5–§7 de este ADR. Su análisis técnico de escritura y huella se conserva entero (§11).styx.jsonPrecisado en el lock: el documento reserva
kinds.<id>para el esquema de cada tipo de contenido, y para los formatos que no se pueden escribir vive en un sidecar.styx/o en el overlay. El audiovisual no cambia. Ver el lock, identidad por layout.
AttachedFile con FileName styx.json y FileMediaType
application/vnd.styx.metadata+json. Va detrás de cover.*, para respetar la convención de
matroska.org de "portada primero" (dec-0126 §2.1).---- con mean dev.mks2508.styx, name doc y data de tipo 1
(UTF-8), dentro del mismo moov/udta/meta/ilst que dec-0126 §2.2. Así cabe en el mismo
padding y lo cubre el mismo journal.dec-0126 P4. Lo que no cabe (reparto completo, por ejemplo) se recorta por prioridad
declarada en el esquema y se marca truncated.styx-metadata/v1, TypeBox en @styx/domain, contract-first, dec-0116). Los
nombres finales los fija el ticket con su consumidor:| Bloque | Contenido |
|---|---|
| cabecera | schema, workUid (UUIDv7 generado en la primera escritura, §6.4), kind (movie, episode…), edition (key, name, kind de Edition) |
| obra | títulos (con idioma), fecha, géneros, sinopsis corta y larga, clasificación, colección pública (por ejemplo la de TMDb), serie/temporada/episodio, reparto y equipo por nombre e id externo, sin ids internos |
| ids | imdb, tmdb, tvdb2 con los formatos de Matroska (dec-0126 §2.1), y otros proveedores por espacio de nombres |
| artwork | lista de {role, sha256, bytes, w, h, mime, embedded, source:{provider, ref}}, donde role es cover, cover_land, backdrop, logo, thumb… embedded: true sólo para la portada (§8.1) |
| marcas | markers: [{kind: intro|credits|recap|preview, startMs, endMs, source, confidence}]. Se proyectan a capítulos nativos (§4.3). Detectarlas es V-N04, fuera de este ADR |
| pistas | etiquetas humanas de pista (nombre, idioma, default/forced), que el contenedor también tiene de forma nativa. Las nativas mandan, y el documento sólo guarda lo que el contenedor no sabe expresar (por ejemplo "comentario del director") |
| curación | locks: [campo] (locks manuales, r48 §3.2) y provenance resumida por campo: {provider, at, confidence}. El historial completo se queda en el catálogo |
dec-0126 §3.2, con dos cambios):
@styx/authz;workUid sí va. Es una identidad de obra que no dice nada de nadie.planEmbed de dec-0126 §3.1
recibe el documento). Usa la tabla fija de dec-0126 §3.2 (©nam, ©day, desc, covr…;
TITLE, DATE_RELEASED… en Matroska).STYX_EMBED_DIGEST (dec-0126 §3.2) pasa a ser el hash de la proyección esperada:
BLAKE3(proyección(documento)). Así se detecta una edición de terceros sin comparar campo a
campo.Chapters/EditionEntry
en Matroska, y en MP4 la lista de capítulos que lee el ecosistema (chpl o pista de texto de
capítulos; el ticket mide cuál leen Chrome, VLC y Apple, sin afirmarlo aquí).dec-0126 §3.3).metadataHash y la detección de cambiosmetadataHash = BLAKE3-256(bytes JCS de styx.json). Se usa BLAKE3 por coherencia con la huella
de dec-0114. Se muestra con prefijo m1- y en base32.sha256. Por tanto, cambiar una imagen cambia el hash
sin hashear imágenes en cada comprobación.STYX_EMBED_DIGEST.dec-0114 §5: (dev, ino, size, mtimeNs) en
local, o etag/mtime/size en remoto. Si no cambió, no se lee nada.styx.json, lo
recanonicaliza y calcula el hash.
touch o
tras una reescritura de dec-0126 §5.2, que cambia ino pero no la huella.inotify/fanotify sobre las raíces locales) como acelerador opcional: sólo
adelanta el nivel 1. El reescaneo periódico sigue siendo la garantía. dec-0126 §13 dejaba el
watcher fuera, y aquí se admite sólo como optimización sin semántica propia.styx library reindex --full): ignora las tuplas y relee los documentos.
Es la reconstrucción de §6.5.Cada dato del modelo pertenece a una clase. El esquema del índice declara la clase de cada columna, y un guard falla si una columna no la declara (§12, gate).
| Clase | Fuente de verdad | Ejemplos | Sobrevive a perder la base de datos |
|---|---|---|---|
| F — fichero | styx.json en el fichero | Work descriptivo, Edition (name, kind, releaseDate, tags), ids externos, artwork (referencias + portada), marcas, locks, workUid | Sí, con un reindex |
| X — facts de contenido | los bytes del fichero, recalculables | MediaIndex (pistas, códecs, duración, keyframes), huella de payload, capítulos nativos ajenos, tamaño | Sí, con un probe (qry.media.probe) |
| D — derivado | una función de F + X; vive en cachés direccionadas por hash | variantes de artwork, trickplay (V-N04), proyecciones de búsqueda, blurhash o thumbhash si se adoptan | Sí, se regenera (r12: coste de regeneración) |
| C — servidor | catalog-svc / sources-svc | raíces y su configuración, SourceBinding (ruta, preferencia, capabilities), ledger de escaneo, cola de revisión, historial completo de provenance, colecciones manuales del servidor, overlay (§6.3) | No: backup de la base de datos |
| U — usuario | catalog-svc, por (actorId, profileId) (§10) | progreso, visto, favoritos, valoraciones, listas, "seguir viendo", preferencias de pista por perfil | No: backup y exportación por perfil |
| K — caché de cliente | el cliente, validada por metadataHash/ETag | lo que la web o la app guardan para no volver a pedir | No aplica: se revalida |
Lo que cambia respecto a dec-0126 §6: los locks y el workUid pasan de "no reconstruible" a
F. El historial de provenance sigue en C, y en F queda sólo su resumen.
fileState) y quién ganaEstado por asset en el índice, inspirado en Lightroom Classic:
fileState | Significa | Qué pasa |
|---|---|---|
synced | el índice refleja el documento (metadataHash igual) | nada |
pendingWrite | hay una edición aceptada (usuario, enriquecimiento) que aún no está en el fichero (debounce de dec-0126 §4.2, lease ocupado, reintento) | el índice muestra el valor nuevo marcado como pendiente. Es un overlay temporal. Al confirmarse la escritura (evt.media.metadataEmbedded con el hash nuevo) pasa a synced |
changedExternally | cambió el fichero y el índice no tenía nada pendiente | gana el fichero. Si cambió styx.json (otra instancia de Styx, una copia retocada, una restauración), el índice se re-deriva sin preguntar y emite evt.catalog.metadataChanged. Si sólo cambió la proyección (un tercero con mp3tag o mkvpropedit), §6.2.1 |
conflict | cambió el fichero y había un pendingWrite | no se escribe nada. Va a la cola de revisión con el diff por campo (fichero contra pendiente), y el usuario elige. Es el único caso que pregunta |
overlay | el fichero no se puede escribir (§6.3) | el catálogo guarda el documento completo como autoridad provisional, marcado |
absent | el fichero no tiene styx.json (nunca incrustado, o raíz con embed off) | la autoridad es el overlay si existe, y si no, lo importado (tags ajenos, NFO) con su confianza |
Sustituido en el lock (P3): el modo por defecto es la adopción inteligente (
externalTagEdits: smart), con los criterios del lock.review,adopteignorequedan como overrides por raíz.
STYX_EMBED_DIGEST no cuadra).externalTagEdits: review), el cambio se ofrece en la cola de revisión como
MetadataPatch con autoridad local (r48 §3.2). Aceptarlo reescribe el documento.externalTagEdits: adopt, por raíz, el patch se aplica solo, salvo en campos con lock.ignore, la proyección se regenera desde el documento en la siguiente escritura.ro, ficheros en seeding, nlink > 1, formatos sin editor y raíces
con embed off: son las exclusiones de dec-0126 §4.3, que siguen vigentes.fileState: overlay. Es la única excepción a "el fichero manda", y es
explícita y visible en la UI y en library.metadata.embedStatus.dec-0121), en la promoción al Storage Box, en la canonicalización (dec-0019), al expirar el
seeding lease, o al activar el embed en la raíz. A partir de ahí el asset pasa a synced, y el
overlay se borra.dec-0131.Precisado en el lock (P6):
assetIdVersion: 1congelado, y la huella de asset definida porlayout(file,treecon raíz de manifiesto sin.styx/,imagey firmados con el hash del fichero entero). Ver el lock.
Un índice desechable con ids aleatorios rompería todo lo que cuelga de él. Por eso:
AssetId = ast_ + base32(BLAKE3(huella de payload v1)) truncado a 128 bits. La huella
es la de dec-0126 §3.3, que no cambia al incrustar.
SourceBinding, que es la deduplicación de
dec-0114.WorkId = derivado del workUid del documento.
workUid se genera una vez, en la primera escritura, salvo que el índice ya tenga una
obra con el mismo id externo: entonces se reutiliza. Así el 4K y el 1080p de una película
comparten obra.workUid distintos.
Se reconcilian por ids externos, y eso es dec-0131.EditionId = derivado de (workUid, edition.key).SourceBindingId sigue siendo de servidor (clase C).absent) reciben su workUid en el overlay, y lo conservan al
materializarlo.dec-0114.styx library reindex --full reconstruye las clases F y X del índice desde los ficheros, y la
D se regenera de forma perezosa. Las clases C y U salen del backup.AssetId/WorkId/EditionId. El estado de usuario
restaurado del backup sigue apuntando a sus obras.Resuelto en el lock (P2): almacén de documentos direccionado por contenido con los bytes JCS exactos, proyección materializada por función pura versionada y estado del fichero como tres punteros de hash. Ver el lock.
metadataHash, fileState, la tupla de observación y
indexedAt.metadataHash. Pesa pocos KB por
obra y hace que la ficha cueste cero lecturas. Es caché: si se borra, se relee. Pregunta P2:
la alternativa es guardar sólo la proyección y leer la ficha del daemon.r28).r48: ningún plugin lee bytes). Su acceso
rápido es el ExtensionContext sobre el índice.| Operación | Lecturas de fichero | Cómo se comprueba |
|---|---|---|
| Listado, búsqueda, rails, ficha, "seguir viendo" (caliente) | 0 | contador styx_catalog_daemon_reads_total{route} en el test del gate: los handlers de consulta no llaman al daemon. Guard estático: no importan el cliente del daemon |
| Item aún no indexado dentro de un listado | 0 en la petición | el item se devuelve con indexState: pending y llega por el feed de cambios (§7.3). Ninguna petición de listado espera a una lectura en frío |
| Página en frío (reindex, primer escaneo, índice perdido) | 1 petición por lotes al daemon (qry.media.readEmbeddedBatch, ≤ 64 items) | el daemon lee en paralelo y con presupuesto (clase de E/S de escaneo); por fichero, §7.4 |
| Comprobación de cambios en un reescaneo | 0 si la tupla no cambió; la región de metadatos si cambió | ledger de escaneo |
| Artwork | 0 lecturas del fichero de vídeo | sale del almacén direccionado por contenido (§8) |
Los tiempos objetivo en frío (por ejemplo, 50 items en disco giratorio frente a NVMe) no se fijan
a ojo: se miden en el gate sobre la biblioteca real de waxin y quedan como presupuesto con
evidencia (r31).
ETag: "m1-<metadataHash>". Una lista lleva
como ETag el hash de su secuencia de (assetId, metadataHash) más su catalogRevision. Con
If-None-Match, la respuesta es 304 sin cuerpo.catalogRevision monotónica por biblioteca, que
avanza con cada cambio de F, X o C visible en la API.
GET /library/changes?since=<rev> (operación headless library.changes, dec-0124 §6.1)
devuelve [(assetId|workId, metadataHash, op)] y la revisión nueva. Es un resumen: el
cliente pide sólo lo que no tiene por hash.evt.catalog.metadataChanged a los clientes conectados con la misma
forma.since es más antiguo que la retención del feed, la respuesta es 410, y el cliente
revalida su caché entera por hash (If-None-Match en lote).assetId/workId con su metadataHash. Un hash igual significa contenido
igual, sin pedir nada. Es la única copia fuera del servidor, que es lo que pide V-DATA-01.styx.json. El cliente offline lee el documento
del propio fichero y no depende de su caché (§9.2).moov va delante. Se lee la ventana de
cabeza que ya lee la huella; si moov es mayor, se hace una lectura más hasta su final. Se
recorre udta/meta/ilst saltando el payload de covr, que se lee sólo para extraer la
portada al almacén (§8.1) la primera vez que aparece su sha256.moov al final: una lectura de cola guiada por el tamaño de las cajas de nivel 0.SeekHead → Attachments. Se lee sólo el AttachedFile styx.json y las
cabeceras de los demás, nunca su FileData. Lo mismo con Tags y Chapters.dec-0126 §5.1 (región de metadatos ≤ 16 MiB) y styx.json ≤ 256 KiB. Pasar
un tope es un error tipado, no una lectura mayor (dec-0117 I5, BoundedReader).(dev, ino, mtimeNs) →
(metadataHash, documento), con eviction de r45. Un reindex repetido no relee.cover_land si P3 de dec-0126 lo amplía), como
covr/cover.jpg. Es lo que ve un reproductor de terceros.dec-0126 §3.1: volumen propio,
escrito por catalog y montado ro en el daemon como raíz I7): todas las imágenes del
documento por sha256, incluida una copia de la portada extraída del fichero.sha256 y su origen
(source: {provider, ref}).
Enmendado en el lock (P4): credencial SCT de scope
artpor sesión, ligada al actor y verificada por el daemon;Cache-Control: privateen vez depublic. Ver el lock.
r12/r45), el camino HTTP/3 y H2, las
raíces I7, la contabilidad por actor y la fuente remota. catalog-svc no sirve bytes./art/<sha256>/<variante>, con Cache-Control: public, max-age=31536000, immutable y
ETag = sha256 de la variante. Un cambio de imagen es un hash nuevo y, por tanto, una URL nueva.
Nunca hay que invalidar.r58 (tamaños por superficie, formato
AVIF o WebP con fallback JPEG). Se direccionan por (sha256 origen, spec de variante).dec-0117 I5/I10). Fuera del
camino de servicio, un fallo del decodificador no tumba el servicio de vídeo.art por sesión, ligada al actor, o proxy por el BFF).Las miniaturas de trickplay (V-N04) son clase D: derivadas de los keyframes del índice X,
direccionadas por (huella de payload, spec), y servidas por el mismo camino que el artwork. No
van en el fichero. El nodo y la detección quedan para su propio diseño (GAP de V-N04).
dec-0126 §3.1:
metadata): busca, normaliza y propone MetadataPatch con
evidencia;embed): proyecta el documento al contenedor.metadata-enrich. El de embed es metadata-embed.V-DATA-04). Se desactiva por biblioteca
(library.root.update --enrich off) y se reemplaza por otro plugin que declare el mismo
slot, vía el SDK de dec-0033/r48 con su nivel de confianza.
metadata-local-nfo), los tags incrustados y el parse de nombre. Cada uno declara egress y,
si la necesita, su clave (ctx.secrets, r48). El orden por campo es el "orden configurado"
de r48 §3.2. Styx trae perfiles preconfigurados (películas, series, anime…) y el usuario
edita el orden.r48 §3.2 (lock manual > autoridad local > orden configurado > confianza). La
autoridad local incluye ahora el styx.json existente. El documento se escribe por el camino de
dec-0126. Activo por defecto no significa escribir por defecto en cualquier raíz: §9.3.styx.json y su
proyección, incluida la portada.
track/media-engine) recibe el documento y lo escribe en el contenedor
de destino con las mismas reglas de §4.absent u overlay) inyecta el documento del
overlay en la salida si el formato lo permite sin reescribir el payload (MP4 con moov
delante se emite con su moov reescrito en streaming). Si no lo permite, se entrega tal cual y
se avisa. El diseño de transferencias es dec-0129.dec-0126 P2)dec-0126 §4.1 recomendaba metadataEmbed: off. Con el fichero como fuente de verdad, off
significa que esa raíz vive en overlay. La propuesta:
dec-0121, raíces canónicas o normalizadas de
dec-0019, raíces creadas desde la UI de Styx): merge, in-place, allowRewrite: false.merge y explica los riesgos de dec-0126 §10. No se activa sin un sí
explícito. Mientras tanto, la raíz funciona en overlay, sin perder nada.dec-0126 §4.3 no cambian.Enmendado en el lock (P5): el estado de usuario se reparte entre playback-svc y catalog-svc con clave común
(actorId, profileId). El "dueño propuesto: catalog-svc" de abajo queda sustituido por el reparto del lock.
dec-0126 §11, y se mantiene.evt.playback.progress con debounce
(al pausar, al cerrar y cada N segundos, con N en el ticket). catalog-svc lo persiste.(actorId, profileId, workKey | assetKey), con workKey = WorkId y assetKey =
AssetId. Los dos son ids estables (§6.4), así que el estado sobrevive a un reindex y a la
migración de la biblioteca a otro servidor con el mismo contenido.evt.identity.profileDeleted (dec-0125 §12.2).
Exportación por perfil en JSON (operación headless profile.state.export).dec-0128
(dispositivos) y dec-0129 (offline).V-N06): compartir estado entre servidores no se decide aquí. Las claves
estables lo hacen posible (dec-0131).dec-0126)dec-0126 §12 P1: se resuelve como fichero autoritativo con el catálogo como índice
derivado (§3, §6). Se retira la recomendación "catálogo autoritativo".dec-0126 §6: la frase "La base de datos sigue siendo la autoridad de lo que sirve la
API. El fichero es una proyección duradera" se sustituye por: "El fichero es la autoridad
(clase F). El índice del catálogo es una proyección derivada y reconstruible. El overlay
(dec-0127 §6.3) es la autoridad provisional de lo que no se puede escribir". Los locks
manuales y el workUid pasan a la lista de lo reconstruible.dec-0126 §7 ("Sincronización fichero ↔ catálogo: pregunta abierta"): lo sustituyen §5 y
§6.2 de este ADR. Se conservan STYX_EMBED_DIGEST, ahora definido en §4.2, y la cola de
revisión para conflictos.dec-0126 §3.2: el modelo de tags común pasa a ser la proyección de styx.json (§4.2).
Se añade el documento como adjunto o item ----:dev.mks2508.styx:doc.dec-0126 §12 P2: la recomendación cambia de off a la de §9.3 de este ADR.dec-0126 §3.1: el almacén de artwork por contenido se mantiene y gana variantes
derivadas servidas por el daemon (§8).dec-0126: §2 (investigación de formatos), §3.3 (huella sobre payload,
que este ADR usa para AssetId), §3.4 (scope annotate), §4.3 (exclusiones), §5 (in-place con
journal, reescritura con allowRewrite, padding), §10 (riesgos) y §11 (alternativas).dec-0114 §5: el ledger de escaneo guarda además metadataHash y fileState. §3 ya lo
enmienda dec-0126.dec-0126).
Contradice N1: el fichero sería una exportación, y una edición fuera de Styx sería siempre un
conflicto en vez de la verdad..styx.json o NFO junto al fichero. Waxin lo descartó: se desincroniza al mover o
renombrar, y un fichero descargado no lo lleva. Matiz del lock: sigue rechazado para el
audiovisual; se admite sólo para formatos que no se pueden escribir sin romperlos (árboles,
imágenes, ficheros firmados; dec-0132 M3).V-DATA-02 y hace la primera página tan lenta como el disco.merge in-place en las raíces que Styx posee y
asistente con merge propuesto en las raíces existentes (recomendado), o merge en todas sin
preguntar, o off en todas con overlay.styx.json entero como JSONB
direccionado por hash, para que la ficha cueste 0 lecturas (recomendado), o guardar sólo la
proyección y leer la ficha del daemon con su caché L1 (menos duplicado, una lectura en frío por
ficha).review por defecto
(recomendado), adopt o ignore.art por sesión, ligada al actor
(recomendado; el daemon ya verifica SCT), o proxy por el BFF de la web, con un camino aparte
para las apps.AssetId desde la huella de payload y WorkId desde un
workUid incrustado (recomendado), o mantener ids aleatorios y una tabla de correspondencia que
también habría que respaldar.dec-0126 P7 reformulada): enriquecimiento activo
sólo con fuentes sin clave (documento, tags, NFO, nombre) y un asistente que pide la clave de
TMDb en el primer arranque (recomendado), o una clave de Styx incluida (sujeta a los términos
de TMDb y a su atribución).@styx/domain gana el esquema styx-metadata/v1 y las funciones puras de JCS, metadataHash e
ids derivados, con vectores compartidos TS↔Zig (como los de spire).track/media-engine gana la lectura acotada de styx.json (§7.4), su escritura en el editor
de dec-0126 y en el muxer de salida (§9.2), y los capítulos (§4.3).qry.media.readEmbeddedBatch, la caché L1 de documentos y el servicio de
/art/<sha256>/<variante>.fileState, el overlay, la
catalogRevision, library.changes, el estado de usuario (si P5 se resuelve así) y
styx library reindex.evt.catalog.metadataChanged hacia los clientes.track/plugin-seams gana el slot de enriquecimiento activo por defecto y reemplazable.styx_catalog_daemon_reads_total en 0 en las rutas de listado, búsqueda y ficha, con un
mutante que lea del daemon y ponga el test en rojo;styx.json leído de
vuelta sin el catálogo;dec-0124): explicacion/modelo-de-datos (estado especificado) se ata a
este ADR. explicacion/metadatos-en-el-fichero cita este ADR en su pendiente de P1.dec-0128), transferencias y offline
(dec-0129), modo público (dec-0130), federación y reconciliación de workUid entre servidores
(dec-0131), la detección de intro y créditos y el trickplay (V-N04), los nombres finales de
subjects y operaciones, ni el gate de track/domain-media/file-ssot (su alta en styx.model.yml
es propuesta queued, pendiente de ratificar con el lock).Decisión de waxin vía AskUserQuestion (2026-10-02): lockear juntos dec-0126 (mecánica de
escritura de metadatos en el fichero) y este ADR (modelo de datos, metadata SSOT en el fichero).
dec-0126 queda enmendado por este donde chocaban: su P1 "catálogo autoritativo" pasa a
fichero-SSOT y su P2 "off por defecto" pasa a merge en las raíces propias (§11). Respuestas a
§13:
merge en las raíces propias, asistente en las existentesRespuesta de waxin: merge in-place en las raíces que gestiona Styx (ingesta, canónica, creadas desde la UI). En raíces existentes, como la biblioteca Jellyfin, el asistente ofrece activar merge. Queda §9.3 tal cual, sin la marca de pregunta:
metadataEmbed: merge, in-place, allowRewrite: false, montaje
rw-metadata configurado al crearlas;merge en el asistente, que explica los
riesgos de dec-0126 §10 y recomienda styx library prepare (padding) antes;dec-0126 §4.3 no cambian.Elección: almacén de documentos direccionado por contenido + proyección materializada por función pura + estado del fichero como tres punteros de hash.
Almacén de documentos metadata_documents(metadata_hash PK, doc_jcs bytea, size, schema, first_seen_at). Guarda los bytes JCS exactos de styx.json, no un JSONB.
BLAKE3(doc_jcs) = metadata_hash. Un scrub
periódico de baja prioridad lo revalida.bytea y no JSONB. JSONB no conserva los bytes: reordena claves, normaliza
números y descarta duplicados. Para comprobar el hash habría que recanonicalizar en cada
lectura, y servir el documento exigiría reserializarlo. Con los bytes exactos, la ficha se
sirve tal cual, con ETag: "m1-<hash>", sin serializar nada, y la verificación es un hash.
Las consultas no entran en el documento: van a la proyección (punto 2).Proyección materializada (asset_projection, work_projection, la búsqueda de texto
completo de §7.1): una función pura y versionada projectStyxDoc@vN(doc, facts) → filas,
en @styx/domain, la misma que usan los tests.
projection_version y el metadata_hash del que sale.vN y se re-proyecta desde el
almacén. Esa es la razón principal para guardar el documento entero. Con "sólo la
proyección", cada columna nueva, cada cambio de vocabulario de géneros y cada índice nuevo
costaría un reescaneo de la biblioteca.Estado del fichero como tres punteros por asset (sustituye a la columna fileState
suelta de §6.2; los estados pasan a ser una función de los punteros, evaluada en el orden
de la tabla):
observed_hash: el documento que hay ahora en el fichero (null si no tiene);intended_hash: el documento que Styx quiere en el fichero (edición aceptada,
enriquecimiento, overlay);base_hash: el observed_hash sobre el que se aceptó la intención.| Estado | Condición |
|---|---|
synced | intended = observed |
pendingWrite | intended ≠ observed y observed = base |
conflict | intended ≠ base y observed ≠ base |
changedExternally | intended = base y observed ≠ base: se adopta, intended := observed |
overlay | intended no nulo y asset no escribible (§6.3) |
absent | observed e intended nulos |
El caso conflict tiene una base de merge de tres vías por campo (base, observed,
intended): el diff por campo de la cola de revisión sale de ahí, y P3 lo usa para no
preguntar cuando los dos lados tocaron campos distintos.
evt.media.metadataEmbedded con el hash nuevo hace
observed := base := intended. No hay estados que se desincronicen de los datos.intended_hash que no se puede materializar.Presupuestos que cumple: la ficha cuesta una lectura por clave primaria en el almacén, con 0 lecturas de fichero. Un listado sólo toca la proyección. La caché de cliente valida por el mismo hash que es la clave. El índice sigue siendo desechable: perder las tablas F/X/D es un reindex (§6.5), y el almacén se repuebla con las lecturas por lotes de §7.4.
Alternativa descartada: sólo la proyección y la ficha leída del daemon (caché L1). Duplica menos (pocos KB por obra), pero cada ficha en frío es una lectura de fichero, el ETag de la ficha dependería de la caché del daemon, y cada cambio de proyección exigiría un reescaneo.
Respuesta de waxin: adopción inteligente. Si los tags externos se consideran peores, basura o
erróneos, no se adoptan. Si no, se toman como decisión del usuario y se adoptan, avisándole sólo
cuando haga falta que intervenga. Nuevo valor por defecto externalTagEdits: smart por raíz.
review, adopt e ignore quedan como overrides.
Cuándo se activa: el reescaneo ve que la proyección del fichero no cuadra con
STYX_EMBED_DIGEST (un tercero editó los tags) y styx.json no cambió. Si cambió styx.json,
sigue mandando §6.2: gana el fichero sin preguntar. El mismo filtro se aplica a los tags ajenos de
un fichero absent cuando se importan.
Paso 1, diff por campo. Se compara la proyección leída con la esperada del documento, sólo en
los campos de la tabla fija de dec-0126 §3.2. Los tags ajenos que no están en la tabla se
conservan y no se interpretan.
Paso 2, filtro de validez y basura. Cada campo cambiado se rechaza (no se adopta, sin avisar) si cumple cualquiera de estas condiciones:
styx-metadata/v1: tipo, formato o rango. Por ejemplo, una fecha que no
parsea, un año fuera de [1874, año actual + 5], un IMDB que no es tt + ≥ 7 dígitos, o un
TMDB sin movie/ o tv/.Untitled, Unknown, Movie, Title,
Track 1…).1080p, 2160p, x265, WEB-DL, BluRay,
-GRUPO…), o frases de sitio ("Downloaded from", "www.").Paso 3, contraste con proveedores para los campos de identidad. Son los ids externos, kind y
serie/temporada/episodio. Cambiarlos es reidentificar, no retocar. Se adoptan sólo si el
proveedor configurado confirma el id nuevo y este cuadra con el contenido: la duración del
payload coincide con el runtime del proveedor dentro de ±2 % o ±90 s, y el título o el año
coinciden. Si no hay proveedor con clave (P7), o no cuadra, el cambio va a revisión. Nunca se
rechaza en silencio: un id nuevo puede ser la corrección de una identificación mala.
Paso 4, adopción. Los campos descriptivos que pasan el paso 2 se adoptan aunque difieran del proveedor: son decisión del usuario.
provenance {provider: external-edit, confidence: 1} y con lock manual, para que el enriquecimiento no lo revierta.styx.json nuevo por el camino normal (intended := doc nuevo). Esa escritura
regenera también la proyección de los campos rechazados.Paso 5, aviso sólo si hace falta intervenir. Va a la cola de revisión y se notifica cuando:
styx.json del fichero no valida contra el esquema. El documento se pone en cuarentena, el
índice conserva el último bueno, y se avisa una vez por fichero.Lo adoptado y lo rechazado no notifican. Quedan en el historial de provenance (clase C) y en
library.metadata.embedStatus, con un resumen agregado por escaneo.
Los umbrales (±2 %, ±90 s, 300 px, aspecto, año) son valores iniciales reversibles. El ticket
los calibra con fixtures de ediciones reales (mp3tag, mkvpropedit, AtomicParsley) y con el
corpus de SCAN-01 (dec-0114 §4).
artRespuesta de waxin: SCT con scope art por sesión, ligada al actor y verificada por el daemon.
dec-0117 A4). La entrega el BFF a la web, o
la API a las apps nativas, por sesión de usuario y no por sesión de reproducción. Se renueva por
el mismo camino que la SCT read.dec-0117 §4.1/I3): scope nuevo art, sólo lectura. resource = el
almacén de artwork del nodo, no un assetId. Va ligada al actor (actor = hash del actorId) y
a su acv (dec-0125). Un evt.identity.accessChanged deja de renovarla. Es multiuso, a
diferencia de annotate e ingest.dec-0119 §5), como el resto de
scopes. Transporte: cap= en la URL desde el navegador y Authorization en apps nativas
(dec-0117 §4.1).sha256 sólo llega al cliente dentro de respuestas de la API filtradas por el permiso
del actor (dec-0125), y que 256 bits no se adivinan. Riesgo residual aceptado: un actor que
filtra un hash con su SCT a otro actor autenticado del mismo servidor le da una imagen, nunca
vídeo ni metadatos.Cache-Control: private, max-age=31536000, immutable (no public: va con
credencial). Como cap= forma parte de la URL, la web guarda las imágenes por sha256 en su
propia caché (clase K, §7.3) y no depende de que la URL coincida entre sesiones.Respuesta de waxin: se reparte entre catalog-svc y playback-svc, unidos por usuario y perfil, con una capa SQL bien pensada. Sustituye a "dueño propuesto: catalog-svc" de §10.
Clave común: (actorId, profileId), de dec-0125. Allí la cuenta es el actor ("Cuenta =
quien inicia sesión. Es el actor de hoy"), así que accountId ≡ actorId y no se introduce un
nombre nuevo. En @styx/domain es IProfileRef { actorId, profileId }.
El sujeto es una unión discriminada alineada con dec-0107 (LOCKED en w11/locks):
{kind: 'asset', assetId} | {kind: 'work', workId} | {kind: 'service', liveServiceId}.
AssetId y WorkId son los ids derivados de P6. LiveServiceId es de servidor (clase C,
dec-0107).
Regla de reparto: cada dato lo posee el servicio en cuyo flujo nace.
| Dato | Dueño | Por qué |
|---|---|---|
| Sesiones de reproducción (historial de reproducciones) | playback-svc | nacen al reproducir; playback ya emite las SCT y conoce el dispositivo |
Progreso por asset (posición, duración, rev) | playback-svc | lo escribe el reproductor cada pocos segundos; el resume lo lee playback al crear la sesión |
| Preferencias de pista por perfil (idioma de audio y subtítulos) | playback-svc | se aplican en el PlaybackPlan (r05) y se aprenden de lo que el usuario elige al reproducir |
| Visto (marca, fecha, número de reproducciones), incluido el manual | catalog-svc | es estado de la obra para los rails y para "marcar como visto"; se alimenta de playback |
| Favoritos, valoraciones, listas y ocultar de "seguir viendo" | catalog-svc | son curación del usuario sobre el catálogo |
| Proyección de progreso por obra (fracción, último asset) | catalog-svc | derivada de los eventos de playback; existe para que los rails sean una consulta local |
Capa SQL. Cada servicio tiene su esquema y su rol de Postgres. Ningún rol tiene grants sobre el esquema de otro servicio y no hay joins entre bases de servicios (r17).
playback_sessions(session_id uuidv7 PK, actor_id, profile_id, subject_kind, asset_id, live_service_id, device_id, started_at, ended_at, end_reason);playback_progress(actor_id, profile_id, asset_id, position_ms, duration_ms, rev bigint, session_id, updated_at, PK(actor_id, profile_id, asset_id));playback_track_prefs(actor_id, profile_id, scope_kind, scope_id, audio_lang, sub_lang, sub_mode, updated_at, PK(actor_id, profile_id, scope_kind, scope_id)), donde scope_kind
es global, work o series;playback_outbox: outbox transaccional. El cambio de estado y el evento se escriben en la
misma transacción, y un relay publica por @styx/bus. Nunca se escribe sin evento, ni se
emite un evento sin escritura.user_state, separado de las tablas del índice; un reindex no lo
toca):
profile_subject_state(actor_id, profile_id, subject_kind, subject_id, favorite, rating, watched_at, play_count, hidden_from_continue, rev, updated_at, PK(actor_id, profile_id, subject_kind, subject_id));user_lists(list_id, actor_id, profile_id, name, created_at) y
user_list_items(list_id, subject_kind, subject_id, position, added_at);progress_projection(actor_id, profile_id, asset_id, work_id, position_ms, duration_ms, fraction, source_rev, updated_at): proyección local reconstruible;profile_state_revision(actor_id, profile_id, rev): la revisión por perfil del feed de §10;consumed_events(event_id PK): inbox de idempotencia.evt.catalog.* del estado de usuario.Cómo se cruzan sin acoplarse (por bus, con proyecciones locales):
evt.playback.progressed (con debounce: al pausar, al cerrar y cada N
segundos) y evt.playback.completed. "Completado" = la posición pasa el inicio de la marca
credits del documento (§4.3) o el 90 % si no hay marca. catalog-svc consume los dos: actualiza
progress_projection, y con completed fija watched_at y suma play_count.cmd.catalog.markWatched. catalog-svc emite
evt.catalog.watchedChanged, y playback-svc lo consume para poner a cero el progreso de los
assets de esa obra.event_id en el inbox, y last-writer-wins por rev monotónico por
(actorId, profileId, assetId). Un evento con rev menor que source_rev se descarta.progress_projection es derivada. Si se pierde, se repuebla con
qry.playback.progressSince(rev), paginado. Las tablas de las que catalog-svc es dueño salen de
su backup.profile_subject_state), con 0 llamadas a playback y 0 lecturas de
fichero (§7.2). El resume de una sesión es una lectura local de playback-svc.dec-0125 §12.2): los dos servicios consumen
evt.identity.profileDeleted y purgan su parte. profile.state.export compone las dos
exportaciones en el BFF o el CLI, no en un servicio.Los nombres de subjects y tablas son provisionales: los fija el ticket con su consumidor, como el resto de este ADR. Lo que fija el lock es el reparto, la clave y el patrón outbox → bus → inbox → proyección.
Respuesta de waxin: AssetId de la huella del payload y WorkId de un workUid guardado en el
fichero. Queda §6.4 tal cual para el audiovisual, con dos precisiones que fija el lock:
AssetId queda congelado. Se fija como assetIdVersion: 1.
fingerprintVersion posterior (dec-0114 §5) sirve para identificar y deduplicar, pero
no vuelve a calcular AssetId. Si cambiara, cambiarían todos los ids y se rompería el
estado de usuario.assetIdVersion: 2 sería un ADR con su propia migración.dec-0132)waxin quiere un Styx multi-tipo: juegos de PS4 y PS5 (dumps en carpeta, imagen exFAT o .pkg),
copias de PC, música y libros. El diseño está en dec-0132 (PROPOSED, rama w12/multikind, punto
M3). Para no cerrar esa puerta, el lock fija tres cosas. Para el audiovisual (MKV, MP4) nada
cambia respecto a §4–§9. Para los tipos no audiovisuales, este apartado prevalece sobre el resto
del texto.
Huella de asset y AssetId por layout. AssetId = ast_ + base32(BLAKE3(huella de asset)), truncado a 128 bits, donde la huella depende del layout del asset:
layout | Huella de asset |
|---|---|
file audiovisual | la huella de payload de dec-0126 §3.3 (mdat / Clusters + facts en lista blanca). Es la de hoy |
file de otro tipo | la que declare el plugin del tipo, con la misma regla: los metadatos que Styx escribe no entran. Si el tipo no declara ninguna, el hash del fichero entero |
tree (directorio con N ficheros) | la raíz del manifiesto canónico: BLAKE3 del JCS de [{relPath, size, mode, raíz Merkle del fichero}], ordenado por relPath en bytes UTF-8, con relPath en NFC, sin .. ni rutas absolutas, y sin seguir symlinks. .styx/ queda fuera del manifiesto: escribir metadatos no cambia la identidad |
image, y formatos firmados o de sólo lectura (un .pkg de PS4, una imagen exFAT, una .iso) | el hash del fichero entero (la raíz de transferencia de dec-0129 §3.2 cuando exista). El árbol interior que saque una etapa de desempaquetado es una faceta, no la identidad |
Cada variante se versiona en assetIdVersion con su layout. La regla de no volver a calcular
el id es la misma para todas.
styx.json reserva kinds.<id>. La cabecera y el bloque común de §4.1 (schema,
workUid, kind, edition, títulos, fecha, ids externos, artwork y curación) valen para todos
los tipos. Lo propio de cada uno va en kinds.<id>, con el esquema versionado que registre su
plugin. Por ejemplo, kinds["game.ps5"] o kinds["book"]. Los ids externos siguen yendo por
espacio de nombres (igdb, ps.titleId, isbn, musicbrainz…). styx-metadata/v1 declara
kinds como un mapa abierto validado por registro: un bloque de un tipo desconocido se conserva
intacto, se ignora al proyectar y entra en el hash del documento.
La fuente de verdad, por formato. La metadata vive en el fichero cuando el formato lo
permite. Cuando no, vive en un sidecar .styx/ o en el overlay del catálogo:
truth | Cuándo | Dónde vive styx.json |
|---|---|---|
embed | el formato tiene sitio y Styx sabe escribirlo sin romperlo (MKV y MP4 hoy; otros, con el tipo que lo traiga) | dentro del fichero, por el camino de dec-0126 |
sidecar | un árbol escribible, o un fichero que no admite escritura segura en una raíz escribible | .styx/styx.json en la raíz del árbol (fuera del manifiesto), o el sidecar junto al fichero que fije dec-0132 |
overlay | lo que no se puede tocar: fichero firmado (un .pkg de PS4 se invalida si se escribe dentro), fuente remota, raíz ro, seeding, nlink > 1 | en el catálogo (§6.3); se materializa en sidecar en cuanto el asset pase a una raíz escribible |
metadataHash, los tres punteros de P2 y la
adopción de P3.Lo demás de dec-0132 (registro de kinds, facetas, acciones, pipelines, enmienda a dec-0107)
sigue PROPOSED y no lo decide este lock.
Respuesta de waxin: proveedores sin clave por defecto (documento incrustado, tags, NFO y nombre de fichero) más un asistente de primer arranque que pide la clave de TMDb. El enriquecimiento (§9.1) está activo desde el primer arranque solo con esos proveedores. Con la clave, TMDb entra en el orden por campo del perfil de biblioteca. Sin ella, nada sale a la red. Styx no incluye una clave propia.
dec-0125 (cuentas, PROPOSED): compatible en la clave ((actorId, profileId), con
accountId ≡ actorId). Choca en un punto: §12.2 y §7.2 hablan de un "dueño del estado por
perfil" (cmd.<dueño>.transferProfileState, evt.identity.profileDeleted hacia un
consumidor). Con el reparto de P5 hay dos dueños. Enmienda que el lock de dec-0125 debe
recoger: transferProfileState se manda a playback-svc y a catalog-svc, e identity-svc
borra el perfil de origen sólo cuando los dos confirman (saga con dos pasos idempotentes).
profileDeleted ya es un evt, así que sólo cambia la lista de consumidores. Su §19 ("el
estado por perfil necesita dueño") queda resuelto por P5.dec-0107 (LOCKED en w11/locks): no hay choque.
Work queda como media finita, y styx.json.kind sólo toma valores finitos (movie,
episode…). Ningún documento lleva live ni channel.service (LiveServiceId) para canales favoritos,
con la diferencia de que su id es de servidor (clase C) y no sobrevive a perder la base de
datos como sí lo hacen AssetId y WorkId.dec-0107 (un Programme grabado pasa a Work → Edition → MediaAsset) escribe en una raíz propia y, por P1, nace con su styx.json.dec-0114 (LOCKED): enmendado (§11): §3 por dec-0126 §3.3 y §5 (el ledger guarda además
metadataHash, y el estado de los tres punteros de P2 vive con el índice). Su "no decide el
watcher" se mantiene: aquí el watcher es sólo un acelerador (§5.2). Su "no cambia el modelo del
catálogo (r08)" se mantiene: cambian los ids, no las entidades.dec-0117 (LOCKED): enmendado §4.1/I3 con el scope art (P4), además de annotate e I8
de dec-0126.dec-0132 (multi-tipo, PROPOSED en w12/multikind): el lock recoge su M3 (identidad por
layout, kinds.<id>, fuente de verdad embed | sidecar | overlay). El sidecar que admite para
los formatos no escribibles matiza el rechazo de §12, que se mantiene para el audiovisual. Su
enmienda a dec-0107 (kinds como registro validado) no la decide este lock.dec-0126 lleva amendedOrSupersededBy: [dec-0127] y, desde el lock, banners en §4.1, §6 y §7
y su propia sección de lock.dec-0114 y dec-0117 llevan banner de enmienda desde el lock (2026-10-02).docs/overview/vision-ledger.yml: V-N01, V-DATA-01, V-DATA-02, V-DATA-03, V-DATA-04 y
V-DATA-05 mapean a este ADR; con el lock pasan a decision-locked.dec-0132 (PROPOSED, w12/multikind): su M3 queda recogido en el lock.dec-0114 (huella y ledger), dec-0121 (ingesta), dec-0125 (clave del estado
por perfil), dec-0019 (canonicalización), r58 (asset contracts).