Vista generada de dec-0114: Escaneo e identificación por contenido como plugin sobre plugin-seams
docs/decisions/dec-0114-scanner-por-contenido-como-plugin.mdVista generada desde
docs/decisions/dec-0114-scanner-por-contenido-como-plugin.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-09-28 |
| Fichero | docs/decisions/dec-0114-scanner-por-contenido-como-plugin.md |
Enmienda a: r48
Enmendado o sustituido por: dec-0126, dec-0127, dec-0132
Por qué importa (del frontmatter del ADR):
Gobierna dónde y cómo vive el escaneo e identificación por contenido: como extension point
scannersobre plugin-seams (r48/r49), con huella de contenido calculada en el daemon vía range reads deSeekableMediaSource, confianza puntuada y explicada, y reescaneo incremental. Sin este ADR, un executor leería r48 L2 («scanner explícitamente FUERA») y no lo haría, o lo metería en el core de catalog-svc por ruta de fichero (el modelo de Jellyfin que queremos superar), o calcularía huellas leyendo bytes de vídeo en TS (viola r01).
Nodos del roadmap que lo citan en refs: track/plugin-seams/w2
Páginas de la documentación que lo citan: Plugins y fuentes (implementado), Servidores conectados y Jellyfin (especificado), Metadatos en el fichero (especificado), Modelo de datos (especificado), Importar NFO de Jellyfin (especificado), Activar los metadatos incrustados (especificado), Raíces y fuentes (especificado), Plugins (implementado), Visión (especificado), Primera biblioteca (especificado)
Leído de docs/decisions/dec-0114-scanner-por-contenido-como-plugin.md, el fichero canónico.
dec-0126 y dec-0127 (LOCKED juntos, 2026-10-02):
mdat / Clusters) más facts en lista
blanca, no sobre las ventanas de cabeza y cola del fichero. "Tamaño exacto" pasa a
payloadSize, el hash completo opcional pasa a ser del payload, y el OpenSubtitles se toma
antes de la primera escritura (dec-0126 §3.3). Para árboles, imágenes y ficheros firmados,
la huella de asset va por layout (lock de dec-0127).metadataHash y el estado del documento (los tres
punteros del lock de dec-0127, P2). Una fingerprintVersion nueva no cambia el AssetId
(assetIdVersion: 1, dec-0127 P6).AskUserQuestion (2026-09-28): escaneo/identificación por contenido:
SÍ, como PLUGIN sobre plugin-seams (r48/r49, packages/plugin-sdk, packages/plugins/*).r48 L2 («Scanner explícitamente FUERA (discovery→identification→mutation = otra
dimensión; sprint posterior)»). El scanner deja de estar fuera: este ADR es ese sprint posterior.r48 §3.2/§3.3 (manifest, registry, precedencia, MetadataPatch declarativo), r49
(precedencia del manifest), r04 (SeekableMediaSource, ReadPurpose), r01 (bytes de vídeo
nunca por JS), r08 (catálogo Work → Edition → MediaAsset → SourceBinding), r18/r20
(contract-first Arktype, bus cmd/qry/evt), r28 §3 (anti-especulativo), dec-0112 (SCAN-01),
dec-0110 (MediaEngine.probe).dec-0112; en la numeración final de las tandas 2/3 este ADR
queda en dec-0114 (dec-0112 = gate WC-JF12, dec-0113 = identity token model). Tabla
completa en la cabecera de dec-0110.Enmendado por
dec-0132(LOCKED, 2026-10-02): libros, cómics y música dejan de estar fuera y se abordan con pluginscontent-type. La huella de §3 se generaliza por layout (fichero, árbol o imagen;dec-0132§4.1).IdentificationProposal.kindes unWorkKindIdregistrado.
catalog-svc escanea por ruta: CatalogHandler.scanAsset recibe un path absoluto,
ffprobe (vía workers-svc) → MediaIndex → DB → evento. La identidad del asset es la ruta; un
rename o un move es un asset nuevo. Es el modelo de Jellyfin (y su punto débil con carpetas
desordenadas y mal nombradas).r48 L2 sacó el scanner del sprint de 4 módulos porque discovery → identification → mutation
era «otra dimensión». La wave 1 de track/plugin-seams (w1) dejó el seam hecho: manifest,
registry con precedencia explícita, lifecycle, health, y dos consumidores reales
(source-local, metadata-local-nfo). PluginKind hoy: metadata | search | source | seed.jellyfin12-design (escaneo inicial ≤ 1/3, reescaneo sin cambios
< 5 s, ≥ 3x menos errores de identificación sobre corpus mal nombrado) sólo es alcanzable si la
identidad es de contenido y el reescaneo es incremental.| Etapa | Quién | Qué |
|---|---|---|
| Discovery | core (orquestador de escaneo del catálogo) | enumera entradas de cada SourceBinding vía la capability de enumeración de la fuente; mantiene el ledger de escaneo; decide qué hay que (re)procesar |
| Huella | daemon Zig, pedida por el core | range reads sobre SeekableMediaSource con purpose: 'probe' + MediaEngine.probe (dec-0110); devuelve digests y facts, nunca bytes |
| Identificación | plugin(s) scanner | puntúa candidatos a partir de huella + facts + señales (§4) y devuelve propuestas con evidencia |
| Mutación | core | aplica propuestas como MetadataPatch/binding con la precedencia de r48 §3.2; el plugin nunca escribe |
r01: la cabecera y la cola de un fichero de vídeo
son bytes de vídeo. El plugin TS recibe hashes y facts; no ve el payload.scannerscanner a PluginExportsByKind en packages/plugin-sdk con su interfaz
(IScannerProvider, nombre final en el ticket) en el mismo cambio que su primer consumidor
real, como exige el propio manifest.ts («imposible registrar un kind sin interfaz»).identify(input) → Result<IdentificationProposal[], ResultError<'SCANNER_IDENTIFY_FAILED'>>
donde input = { locator, sourceBindingId, fingerprint, containerFacts, hints } y hints
son señales baratas del core (nombre de fichero, ruta relativa, sidecars presentes).IdentificationProposal = { target: WorkRef | EditionRef | externalIds, kind: movie | episode | extra | …, confidence: 0..1, evidence: Evidence[], providerIdentity }; cada
Evidence = { signal, value, weight, contribution } para que la puntuación sea
reproducible y explicable.match(fingerprint) → known asset? para plugins con base de huellas propia.ctx.config/ctx.secrets e identidad de
observabilidad son los de w1, sin variantes: un scanner caído degrada la identificación, no
el escaneo (el asset entra como «no identificado»).@styx/api-contracts): comandos y eventos de
escaneo (cmd.catalog.scan…, evt.catalog.asset…) se definen en el ticket; este ADR sólo fija
que la orquestación es del core y que las propuestas viajan como datos, no como mutaciones.fingerprintVersion):
MediaEngine.probe: duración, pistas, códecs, extradata)
— estable ante remux del mismo contenido a otro nombre, distinto ante recodificación;background-cache, sólo para
deduplicación exacta.dec-0109
midió ~66 KB de mediana para los facts), nunca el fichero entero en el camino de escaneo.SourceBinding nuevo sobre el mismo MediaAsset) sin reidentificar.dec-0112).≥ τ_auto se aplica solo; entre τ_review y τ_auto va a cola de revisión;
por debajo, «no identificado». Los umbrales se calibran sobre el corpus etiquetado de
SCAN-01 (300 casos mal nombrados), no se fijan a ojo.r48 §3.2): manual lock > autoridad local > orden configurado > confianza >
fallback. Un scanner nunca pisa un lock manual ni una edición del usuario.r48 §3.3): «por qué
este fichero es esta película» es consultable.(sourceBindingId, locator), tupla barata
de cambio de la fuente (tamaño + mtime + inode/etag según la capability), fingerprintVersion,
huella, resultado de identificación y versión del plugin que la produjo.dec-0112)Objetivos pre-registrados v1, con las reglas de dec-0112 (mismo host, corpus por hash de clips
mínimos válidos, proveedores remotos desactivados en ambos lados, medición tras la migración de
la 12.0, 5 corridas, IC95):
r48 §3.3 (patch declarativo,
merge del core).r01; además duplicaría la ruta de
lectura que el daemon ya optimiza (caché por niveles, cancelación).track/plugin-seams gana una wave nueva (propuesta w2, después de cerrar w1) con el kind
scanner, un plugin first-party y el orquestador de escaneo del core.MediaEngine.probe de dec-0110); hasta que el
motor exista, la huella puede empezar con los hashes de ventana (que no necesitan demux) y
añadir el digest de facts cuando aterrice el probe.CatalogHandler.scanAsset por ruta pasa a ser un caso particular (una entrada del ledger) del
escaneo por SourceBinding.docs/track/plugin-seams/plans/scanner-plugin-proposal.md.styx.model.yml (gobernanza posterior).r08): sólo cómo se llega a sus entidades.r48 L2 lleva banner de enmienda apuntando aquí (el mismo banner cita dec-0112 para L4).dec-0112 (SCAN-01 bajo sus reglas), dec-0110 (MediaEngine.probe).