Arquitectura¶
Cómo está montado el servidor: las capas hexagonales bajo internal/, los
contextos delimitados, los eventos que los conectan y las guardas estáticas
que hacen cumplir esas reglas en el código. El código está en internal/domain,
internal/app, internal/adapters/{driving,driven} y se conecta en
cmd/evey/wire*.go; la organización la decide
ADR-0001, la separación de los
repositorios ADR-0009 y el bus de eventos
ADR-0010. Para compilar y ejecutar, lee
DEV.md; para saber qué archivos tocar en un cambio concreto, lee
el Mapa del código.
El backend está en Go; los clientes web, móvil y TV son un monorepo
TypeScript/React en frontend/ (React Native / Expo para móvil y TV).
Capas¶
flowchart LR
driving[Adaptadores de entrada<br/>internal/adapters/driving<br/>grpcapi, http, pluginhost]
app[Servicios de aplicación<br/>internal/app/context]
domain[Dominio<br/>internal/domain/context<br/>modelos, objetos de valor, puertos]
driven[Adaptadores de salida<br/>internal/adapters/driven<br/>postgres, ffmpeg, mkvgo, ...]
driving --> app --> domain
driven -. implementan los puertos .-> domain
| Carpeta | Contiene | Regla |
|---|---|---|
internal/domain/<context>/ |
los modelos, los objetos de valor y los puertos (interfaces) | ningún import de framework, ningún import de otro contexto |
internal/app/<context>/ |
los servicios de aplicación que orquestan los puertos | toda entrada/salida pasa por un puerto; dlna (SSDP / UPnP AVTransport) y loadtest (su propio cliente HTTP) son las dos excepciones deliberadas que acceden directamente a la red |
internal/adapters/driven/ |
los adaptadores de salida que implementan los puertos: postgres, meilisearch, ffmpeg, mkvgo, filesystem, prober, opensubtitles, email, webpush, webhooksender, pluginrunner, metadata, introproviders, search |
tres más cumplen una interfaz de la capa de aplicación en lugar de un puerto del dominio: itunes y musicbrainz (app/music.AlbumArtFetcher), ociregistry (app/updatecheck) |
internal/adapters/driving/ |
los adaptadores de entrada: grpcapi (+ HTTP de grpc-gateway), http (archivos estáticos, middleware de autenticación, SSE, HLS, recursos de los iframes de plugins), pluginhost (servidor gRPC en un socket Unix, autenticado por las credenciales del proceso que se conecta: la API del servidor a la que llaman los plugins desde su sandbox) |
|
internal/pkg/ |
el núcleo técnico compartido: auth, logger, eventbus, i18n, metrics, tracing, ratelimit, procfork, ssrf, cache, ... | |
internal/config/ |
la configuración del servidor | |
gen/ |
el código proto generado |
Regla de dependencias: adapters -> app -> domain. El dominio nunca importa
la aplicación ni los adaptadores. La composición está escrita a mano (no con
google/wire) en cmd/evey/wire.go y los archivos relacionados wire_infra.go,
wire_services.go, wire_adapters.go, wire_events.go, wire_helpers.go.
Los tests prescinden de los adaptadores inyectando dobles que implementan los
mismos puertos del dominio: 20 de los 21 paquetes del dominio incluyen un
subpaquete fakes/.
Contextos delimitados¶
Treinta y dos paquetes de contexto están en internal/domain/ e
internal/app/: 19 existen en las dos capas, 2 solo en el dominio
(ratings, shared) y 11 solo en la aplicación. Colaboran en parte mediante
eventos de dominio en un bus compartido; la capa de aplicación también lee
directamente el paquete de dominio de un contexto vecino (consulta
Relaciones).
| Contexto | Responsabilidad |
|---|---|
user |
cuentas, autenticación, sesiones, preferencias |
library |
bibliotecas, carpetas, trabajos de escaneo, índice de medios, personas, colecciones, los favoritos y los pulgares de un perfil (los pulgares no tienen servicio de aplicación: el RPC de valoración guarda y lee UserRatingRepository tal cual), trickplay, detección de intros y créditos |
media (solo aplicación) |
enlaces para compartir y tokens de streaming |
mediarequest |
solicitudes de medios enviadas por los usuarios |
metadata |
enriquecimiento con TMDB / OMDb, imágenes |
playback |
flujos, progreso, control remoto, reproduciendo ahora |
transcoding |
trabajos de ffmpeg, segmentos HLS |
subtitle |
integrados + externos + opensubtitles |
mkv |
operaciones con el contenedor MKV (con mkvgo) |
music |
reproducción y etiquetado propios del audio |
photo |
bibliotecas de fotos |
favorite (solo aplicación) |
favoritos por perfil; el modelo y FavoriteRepository están en domain/library |
playlist |
listas de reproducción de los usuarios, listas inteligentes |
collection (solo aplicación) |
colecciones preparadas por el administrador; sus puertos están en domain/library |
ratings (solo dominio) |
traduce las clasificaciones de cada país (CSA, FSK, MPAA, BBFC, ACB) a un único eje de edad mínima |
feed |
feed de actividad / descubrir |
activity |
cronología de la actividad del usuario |
chromecast |
ciclo de vida de las sesiones de envío |
syncplay |
sesiones sincronizadas entre varios dispositivos |
plugin |
registro de plugins, tokens del sandbox, ciclo de vida |
notification |
notificaciones dentro de la aplicación + webpush |
webhook |
webhooks salientes |
backup |
copia de seguridad y restauración de la base de datos + la configuración |
config (solo aplicación) |
ajustes del servidor guardados, escritos tanto desde la entrada REST como desde la gRPC |
dlna (solo aplicación) |
descubrimiento SSDP y control UPnP AVTransport |
loadtest (solo aplicación) |
simula espectadores contra la propia pila HTTP del servidor: HLS, o el archivo tal cual cuando el veredicto de un cliente es una lectura directa |
maintenance (solo aplicación) |
mantenimiento de medios lanzado por el administrador: reconstrucción del índice de posicionamiento, diario de reparaciones, deshacer, integridad del audio |
presence (solo aplicación) |
registro en memoria, no guardado, de quién está conectado ahora mismo |
qoe (solo aplicación) |
recepción y agregación de la telemetría de reproducción CMCD (CTA-5004) |
demo (solo aplicación) |
la instancia pública de demostración (EVEY_DEMO=1): crea sus cuentas, sus bibliotecas y sus medios de licencia libre con los propios servicios del producto, y se reinicia sola |
updatecheck (solo aplicación) |
compara la versión en marcha con lo que publica el registro y se lo indica al administrador; nunca actualiza nada |
shared (solo dominio) |
puertos transversales (EventBus, TransactionalUnit, ServerConfigRepository, ShareLinkRepository), ids tipados, cursores, tokens de streaming |
Relaciones¶
| Emisor | Eventos | Los consume |
|---|---|---|
library |
LibraryCreated / LibraryDeleted, LibraryScanStarted / LibraryScanCompleted, MediaItemAdded, MediaItemRemoved, TrickplayGenerated |
media.item.added: notification, metadata (enriquecimiento automático), el propio library (trabajador de trickplay + detección de intros), feed (invalidación de la caché de géneros) |
playback |
PlaybackStarted / PlaybackStopped |
hoy ningún suscriptor específico; solo los recibe el comodín "*" de webhook |
user |
UserCreated, user.password.reset_requested |
el primero no tiene suscriptor; el segundo lo consume el adaptador de salida email |
pluginrunner |
plugin.error / plugin.started |
app/plugin para su panel de seguridad, notification para el administrador |
shared |
ServerConfigChanged |
los servicios que aplican sin reiniciar un ajuste guardado en el panel de administración (SMTP, zona horaria, presupuesto de CPU, ...) |
- La indexación para la búsqueda no va por eventos:
library.ScanServicellama directamente al puertolibrary.SearchIndex. - No hay ningún puente que lleve los eventos de dominio a los plugins: los plugins llegan a la API con JWT de alcance limitado (ADR-0003). El bus en sí es ADR-0010.
shared.TransactionalUnitenvuelve las escrituras que afectan a varios agregados (ADR-0002).
Ningún paquete de internal/domain importa el paquete de dominio de otro
contexto, una regla que impone internal/contractguard/domain_imports_test.go
(consulta Guardas de arquitectura). La capa de
aplicación no está sujeta a ella: hoy 15 paquetes de aplicación importan el
paquete de dominio de un contexto vecino.
Flujo de datos típico - "un usuario reproduce un medio"¶
- El cliente móvil o web llama a
POST /api/v1/playback/start(ruta de grpc-gateway). internal/adapters/driving/httpejecuta su middleware de autenticación (auth_middleware.go,httpAuthMiddleware) y extrae la sesión.internal/adapters/driving/grpcapi(playback_handler.go) valida el proto y llama aapp/playback.CommandService.StartStream(separación entre órdenes y consultas, consulta ADR-0009).app/playback.CommandServicellama a los puertos del dominio:user.UserRepository,playback.SessionQueryRepo(idempotencia + límite de dispositivos),playback.SessionCommandRepo, y despuésshared.EventBus.Publish(PlaybackStarted).- Los adaptadores de salida hacen el trabajo:
postgresguarda la sesión dentro de un ámbitoshared.TransactionalUnit.Do; el bus interno del proceso (internal/pkg/eventbus) reparte de forma asíncrona. PlaybackStartedno tiene hoy ningún suscriptor específico: solo lo recibe el comodín"*"dewebhook.- La respuesta vuelve por grpc-gateway como JSON; las actualizaciones del progreso circulan por SSE / flujos gRPC.
Lo que pasa cuando el reproductor pide el manifiesto y los segmentos es el tema de Servir un vídeo - el camino de reproducción HLS.
Guardas de arquitectura¶
Las reglas de arriba no son solo documentación. internal/contractguard/
contiene 58 tests estáticos repartidos en 30 archivos que hacen fallar la
compilación cuando el código se aparta de ellas.
La regla hexagonal en sí es TestDomainContextsDoNotImportEachOther
(ADR-0001), en domain_imports_test.go. Recorre internal/domain e impone
exactamente esto: un contexto puede importar internal/domain/shared y su
propio árbol de paquetes, nada más de internal/domain, y nunca
internal/app ni internal/adapters. Su alcance termina en
internal/domain: no dice nada de la capa de aplicación.
Las demás vigilan contratos que cruzan la frontera de un lenguaje o de un proceso, entre ellas:
| Guarda | Lo que rechaza |
|---|---|
TestNoClientReadOfAFieldTheServerNeverWrites |
un campo de respuesta proto que lee un cliente y que ningún código del servidor escribe |
TestRegisteredHandlersGetEveryPortTheyDeclare, TestBootInstallsItsSilentWirings |
conexiones que, si no, fallarían sin avisar |
TestPostgresRepositoriesBoundTheirQueries |
una consulta de un repositorio Postgres sin tiempo límite |
TestProcfork_EveryForkGoesThroughCommand |
un proceso hijo lanzado sin pasar por internal/pkg/procfork |
TestEverySortFieldTheAPIAcceptsHasASQLColumn, ...CanBuildACursorValue |
un campo de ordenación que falta en la tabla de columnas SQL o en el codificador de cursores |
TestEveryWebSurfaceReachingAnAdminRPCIsClassified, más su equivalente nativo para móvil y TV |
una superficie de la interfaz que llama a un RPC reservado a los administradores sin decidir quién la ve |
TestEveryAllowedSlotIsMountedOnWebAndMobile, TestPluginBridgeParityWebMobile |
un slot de interfaz de plugin o un mensaje del puente presente en un solo cliente |
TestImageSubtitleCodecsAgreeAcrossServerAndClients |
una segunda lista de códecs de subtítulos en imagen en un reproductor |
TestHandWrittenClientTypesMatchTheEndpointTheyCall |
un tipo de respuesta escrito a mano en móvil o TV que se aparta de su endpoint |
TestOnlyThePlanDecidesPlayback, TestNoCallerDerivesDownstreamOfThePlaybackPlan |
una decisión de reproducción tomada fuera del resolutor del plan (ADR-0030) |
TestRestartExitCodeAgreesWithTheEntrypoint |
un código de salida de Go y docker/entrypoint.sh que no coinciden en lo que significa un reinicio |
TestServerEmittedI18nKeysExistInCatalogue, TestI18nPlaceholdersAgreeAcrossLocales |
una clave emitida por el servidor que falta en el catálogo, marcadores que difieren entre idiomas |
Diagramas C4¶
Cuatro diagramas describen Evey con niveles de zoom cada vez mayores,
siguiendo el modelo C4. Los nombres de los nodos son
los nombres reales de los paquetes Go de internal/. Los nombres de los
contenedores vienen de docker-compose.yml para evey-server,
evey-postgres y evey-caddy; evey-web, evey-meilisearch,
evey-mobile y evey-mailcatcher se declaran en
docker-compose.override.yml y solo existen en la topología de desarrollo.
Nivel 1 - Contexto del sistema¶
Quién se comunica con Evey, y a qué servicios de terceros se conecta Evey.
flowchart LR
user[Usuario<br/>cliente web / móvil / TV]
admin[Administrador<br/>interfaz web /admin]
oidc[Proveedor de identidad OIDC<br/>inicio de sesión único opcional]
tmdb[API de TMDB<br/>metadatos de películas y series]
omdb[API de OMDb<br/>metadatos de respaldo + valoraciones]
imdb[IMDbAPI / TVmaze<br/>metadatos de series de respaldo]
art[MusicBrainz / iTunes Search<br/>portadas de álbumes]
introdb[IntroDB / TIDB<br/>marcas de tiempo de intros + créditos]
opensubs[OpenSubtitles<br/>búsqueda de subtítulos]
webpush[Pasarela Web Push<br/>FCM / APNs / VAPID]
registry[Registro OCI<br/>etiquetas de imagen publicadas]
subgraph evey[Evey Media Server]
core((Plataforma multimedia autoalojada<br/>biblioteca, reproducción, transcodificación,<br/>música, fotos, plugins))
end
user -->|HTTPS<br/>HLS / SSE / gRPC-Web| evey
admin -->|HTTPS<br/>/admin| evey
evey -.->|flujo de código OIDC| oidc
evey -.->|descarga de metadatos HTTPS| tmdb
evey -.->|descarga de metadatos HTTPS| omdb
evey -.->|descarga de metadatos HTTPS| imdb
evey -.->|descarga de imágenes HTTPS| art
evey -.->|consulta de intros HTTPS| introdb
evey -.->|descarga de subtítulos HTTPS| opensubs
evey -.->|envío push HTTPS| webpush
evey -.->|comprobación de actualizaciones HTTPS| registry
Nivel 2 - Contenedores¶
Las unidades desplegables que arranca docker compose --env-file .env.dev up.
Las flechas continuas son tráfico en funcionamiento; las discontinuas son
conexiones salientes hacia terceros.
flowchart TB
user([Navegador / móvil del usuario])
admin([Navegador del administrador])
subgraph compose[Red de Docker Compose]
caddy[evey-caddy<br/>Caddy 2.9<br/>:80 / :443<br/>TLS, proxy inverso]
web[evey-web<br/>Vite en desarrollo / nginx en producción<br/>:5173 desarrollo<br/>SPA React + RN web]
server[evey-server<br/>binario Go<br/>:9091 HTTP+gRPC-gateway<br/>interno :9090 gRPC]
pg[(evey-postgres<br/>PostgreSQL 16<br/>:5432)]
meili[(evey-meilisearch<br/>Meilisearch 1.12<br/>:7700)]
mobile[evey-mobile<br/>solo desarrollo<br/>:8081 Expo / bundler Metro]
mail[evey-mailcatcher<br/>solo desarrollo<br/>:1025 SMTP / :1080 UI]
end
tmdb[[TMDB / OMDb / OpenSubtitles]]
fs[(volúmenes /media<br/>montajes en solo lectura)]
cache[(volumen evey-cache<br/>transcodificación + trickplay)]
data[(volumen evey-data<br/>claves, registros, copias,<br/>plugins, avatares)]
user -->|HTTPS| caddy
admin -->|HTTPS /admin| caddy
caddy -->|HTTP /| web
caddy -->|HTTP /v1, /api, SSE, HLS| server
web -->|gRPC-Web por /v1| server
user -.->|bundle de desarrollo Metro| mobile
server -->|SQL/TCP 5432| pg
server -->|HTTP 7700<br/>indexación + búsqueda| meili
server -.->|SMTP 1025 desarrollo| mail
server -.->|HTTPS| tmdb
server -->|lectura FS| fs
server -->|lectura/escritura FS| cache
server -->|lectura/escritura FS| data
La producción es distinta de esta topología de desarrollo
El compose de instalación declara un solo servicio, evey. No hay un
contenedor evey-web aparte (ni el mailcatcher ni el bundler Metro, que
son solo de desarrollo): el binario sirve la interfaz web compilada desde
/srv, así que un proxy inverso le reenvía todo, interfaz y API, en
:9091. Postgres y Meilisearch funcionan dentro del mismo contenedor,
supervisados por docker/entrypoint.sh: PostgreSQL 17 con sus datos en
/data/postgres, y el binario de Meilisearch (Dockerfile, etapa
meili-download, fijado en la 1.12.8) con su índice en /data/meili. El
docker-compose.yml del propio repositorio mantiene servicios postgres
y caddy separados, descritos en
Despliegues avanzados. Consulta
Instalación y
Configuraciones de proxy inverso.
Nivel 3 - Componentes (zoom en evey-server)¶
Las capas hexagonales dentro del binario Go. Los adaptadores de entrada llaman a los servicios de aplicación a través de sus interfaces; los servicios de aplicación solo dependen de los puertos del dominio; los adaptadores de salida implementan esos puertos.
flowchart LR
subgraph driving[Adaptadores de entrada]
grpc[grpcapi<br/>internal/adapters/driving/grpcapi<br/>handlers por contexto]
gw[grpc-gateway<br/>JSON/HTTP <-> gRPC<br/>generado desde proto]
http[http<br/>internal/adapters/driving/http<br/>estáticos, SSE, HLS,<br/>auth_middleware.go]
plug[pluginhost<br/>internal/adapters/driving/pluginhost<br/>gRPC por socket Unix<br/>autenticación por credenciales del proceso]
end
subgraph app[Capa de aplicación - internal/app]
direction TB
a_lib[library]
a_med[media]
a_meta[metadata]
a_play[playback]
a_trans[transcoding]
a_sub[subtitle]
a_mkv[mkv]
a_music[music]
a_photo[photo]
a_user[user]
a_upd[updatecheck]
a_misc[favorite, playlist, collection,<br/>feed, activity,<br/>chromecast, syncplay,<br/>plugin, notification, webhook,<br/>backup, dlna, config, mediarequest,<br/>maintenance, presence, qoe, loadtest,<br/>demo]
end
subgraph domain[Núcleo del dominio - internal/domain]
d_models[Agregados + objetos de valor<br/>por contexto delimitado]
d_ports[Puertos de salida<br/>Repos - separación orden/consulta,<br/>EventBus, TransactionalUnit,<br/>ServerConfigRepository]
end
subgraph driven[Adaptadores de salida]
pgad[postgres<br/>repos + migraciones<br/>shared.TransactionalUnit]
meiad[meilisearch<br/>índice de búsqueda]
fsad[filesystem<br/>escáner, analizador, vigilante]
ffad[ffmpeg<br/>transcodificador, trickplay]
mkvad[mkvgo<br/>operaciones MKV]
metaad[metadata<br/>clientes TMDB, OMDb, IMDbAPI,<br/>TVmaze]
introad[introproviders<br/>cadena IntroDB + TIDB]
itunad[itunes<br/>portadas de álbumes]
mbad[musicbrainz<br/>portadas de álbumes]
ociad[ociregistry<br/>etiquetas publicadas]
probead[prober<br/>vía rápida mkvgo,<br/>respaldo ffprobe]
searchad[search<br/>fachada resistente sobre meili]
osad[opensubtitles]
emad[email]
wpad[webpush]
whad[webhooksender]
prad[pluginrunner]
end
grpc --> app
gw --> grpc
http --> app
plug --> app
app --> d_ports
app --> d_models
d_ports -.implementa.- pgad
d_ports -.implementa.- meiad
d_ports -.implementa.- fsad
d_ports -.implementa.- ffad
d_ports -.implementa.- mkvad
d_ports -.implementa.- metaad
d_ports -.implementa.- introad
d_ports -.implementa.- probead
a_music -.interfaz de la capa app.- itunad
a_music -.interfaz de la capa app.- mbad
a_upd -.interfaz de la capa app.- ociad
d_ports -.implementa.- searchad
d_ports -.implementa.- osad
d_ports -.implementa.- emad
d_ports -.implementa.- wpad
d_ports -.implementa.- whad
d_ports -.implementa.- prad
pgad --> pg2[(PostgreSQL)]
meiad --> me2[(Meilisearch)]
fsad --> fs2[(montajes /media)]
ffad --> cc2[(evey-cache + binario ffmpeg)]
metaad -.-> ext2[[TMDB / OMDb / IMDbAPI / TVmaze]]
introad -.-> ext3[[IntroDB / TIDB]]
itunad -.-> ext4[[iTunes Search / MusicBrainz]]
mbad -.-> ext4
ociad -.-> ext5[[Registro OCI]]
Notas sobre las goroutines:
- El servidor gRPC (
grpc.Server.Serve) y el servidor HTTP (gateway + estáticos) funcionan cada uno en sus propias goroutines, lanzadas desde la conexión principal encmd/evey. - Los trabajos de escaneo de
app/libraryfuncionan en un grupo de trabajadores (una goroutine por cada escaneo de biblioteca activo, consultalibrary.ScanService). - Los trabajos de ffmpeg los lanza el adaptador de salida
ffmpeg(ffmpeg.Transcoder.Start) a través deinternal/pkg/procfork, con una goroutine de supervisión por trabajo que vacía el stderr del proceso. shared.EventBusreparte a los suscriptores de forma asíncrona: cada suscriptor funciona en su propia goroutine, así que un consumidor lento nunca bloquea a quien publica. El bus no abre ninguna transacción propia; eso es trabajo del handler.pluginrunnerlanza un subproceso por plugin; los plugins de iframe se ejecutan dentro del navegador del usuario y se comunican con tokens firmados emitidos porapp/plugin.
Nivel 3 (extra) - Zoom en la cadena de escaneo¶
La vía crítica que convierte los archivos del disco en medios enriquecidos y
que se pueden buscar. Las flechas continuas son llamadas directas; las
discontinuas son eventos de dominio en el bus. La indexación para la búsqueda
es una llamada directa a un puerto desde ScanService, no un evento.
flowchart LR
user([El administrador lanza<br/>POST /api/v1/libraries/:id/scan])
subgraph driving2[entrada]
h[grpcapi<br/>library_handler.go]
end
subgraph appLayer[internal/app/library]
svc[ScanService<br/>grupo de trabajadores,<br/>1 goroutine por biblioteca]
end
subgraph drivenScan[salida]
scanner[filesystem.Scanner<br/>recorrido + analizador]
watcher[filesystem.FSWatcher<br/>fsnotify, incremental]
end
subgraph domainScan[domain/library]
repo[(puertos MediaItemCommandRepo + MediaItemQueryRepo)]
bus[(puerto EventBus)]
end
subgraph persist[persistencia de salida]
pgrepo[postgres.MediaItemRepository<br/>dentro de TransactionalUnit]
searchidx[meilisearch.SearchAdapter<br/>implementa library.SearchIndex]
end
subgraph consumers[Consumidores de eventos - cada uno en su goroutine]
meta[app/metadata<br/>enriquecimiento TMDB/OMDb]
trick[app/library<br/>trabajador de trickplay]
intro[app/library<br/>detección de intros]
feed[app/feed<br/>invalidación de la caché de géneros]
notif[app/notification<br/>avisos de medios nuevos]
wh[app/webhook<br/>comodín, POST saliente]
end
user --> h --> svc
svc --> scanner
watcher --> svc
scanner --> svc
svc --> repo
repo --> pgrepo
svc --> searchidx
svc --> bus
bus -. MediaItemAdded .-> meta
bus -. MediaItemAdded .-> trick
bus -. MediaItemAdded .-> intro
bus -. MediaItemAdded .-> feed
bus -. MediaItemAdded .-> notif
bus -. MediaItemAdded .-> wh
trick -. TrickplayGenerated .-> intro
meta --> repo
trick --> ffstore[(evey-cache<br/>sprites de trickplay)]
Lee esto junto con la sección "Flujo de datos típico" de arriba: el camino de reproducción es el camino de lectura simétrico; la cadena de escaneo es el camino de escritura que prepara el índice de medios del que se sirve el camino de lectura.
Para saber más¶
- DEV.md - compilar, ejecutar, migraciones, generación de los proto
- Mapa del código - qué archivos tocar en los cambios habituales
- Servir un vídeo - el camino de reproducción HLS - el camino de lectura que sirve el vídeo HLS, a fondo
- API del servidor - la referencia de la API generada
- Registros de decisión - los registros de decisiones de arquitectura