0028 — Arquitectura GUI/API/frontend: capa de servicios neutral + adaptadores¶
- Estado: Aceptada — firmada por el PO (2026-06-18) ("de acuerdo con el plan; acepto que es más superficie, pero es necesario para la adopción"). Es el TARGET: GUI gateada por #34; el código se construye tras 0027 firmado (ya) y el caso real validado por un tercero.
- Fecha: 2026-06-18
- Gateado por: 0027 (pivote de posicionamiento). No se
baja a
ARCHITECTURE.mdni se escribe código hasta que 0027 se firme. - Enmienda/afecta a: 0021 (el envelope
schema="1", la jerarquía de errores y el mapeo→exit-code suben decli/a una capa de servicios neutral; el CLI pasa a ser un adaptador de esa capa — el contrato externoschema="1"y los exit codes no cambian); 0019 (un server de larga vida reintroduce concurrencia; ver §Operaciones largas — se mantiene single-writer en v1, no se reabre 0019 todavía). - Relacionada con: 0010 (CLI columna),
0029 (workspace = unidad que la API abre),
0005 (extras + núcleo liviano — nuevo extra
[gui]). - Epic: GUI local #34.
Contexto¶
La Nota 12 (revisada 2026-06-18) encuadró la arquitectura: la GUI agrega un 4º frontend (SPA) y una costura nueva de servidor (API local), sin romper "núcleo puro + costuras". El principio es no tener dos implementaciones de la lógica de negocio: CLI y API deben compartir la orquestación.
El encuadre original (2026-06-16) decía que los tres frontends "convergen en run_<cmd>" (en
cli/commands/). Al leer el código (esta sesión) se vio que esa capa de convergencia no existe como
tal:
run_<cmd>(store_path, …) -> dictdevuelve solo el payloaddatay lanza excepciones tipadas (B2GErrorconexit_code/code). Eso sí es reusable.- Pero el contrato del envelope
schema="1", la jerarquía de errores y el mapeo→exit-code viven encli/(_envelope.py/_errors.py), junto con el I/O (print,sys.exit,stderr).
Hacer "converger en run_<cmd>" forzaría a la API a importar de cli/ (y cli deja de ser hoja,
queda atada al otro frontend) o a reimplementar el contrato — drift que ya empezó: el prototipo
app/server/errors.py tiene su propio envelope() duplicado. Además, el contrato que la SPA necesita
no mapea 1:1 a subcomandos (p. ej. scent de un paper, network por (round, kind), search
no tienen subcomando hoy): la convergencia debe ser en servicios, no en comandos.
Decisión¶
Invertir la dependencia (ports & adapters): una capa de servicios neutral de la que CLI y API son adaptadores delgados.
1. Capa de servicios neutral — src/bib2graph/service/¶
Agnóstica de transporte: sin print, sys.exit, Click ni FastAPI. Contiene:
- La orquestación (lo que hoy es
run_<cmd>): operaciones de escritura/lazo (seed,chain,filter,build,enrich,curate,snapshot, …) y funciones de lectura que el CLI nunca expuso (get_scent,get_network(round, kind),search_papers, …) que la SPA sí necesita. - El contrato, subido desde
cli/: el envelopeschema="1"(build_envelope), la jerarquía de errores tipados (B2GErrory subclases) y el mapeo error→código (hoy enhandle_errors).
2. CLI y API = adaptadores¶
- CLI (
cli/): Click +emit/emit_human(I/O) +sys.exit. Llama aservice/, formatea el envelope para stdout y traduce el código aexit code. El contrato externo (envelopeschema="1", exit codes 0–5, ADR 0021) no cambia. - API (
src/bib2graph/api/, costura opt-in): FastAPI delgado que llama aservice/, reusa el mismo envelope y traduce el código a HTTP status. Bind127.0.0.1+ token efímero (C.3 de Nota 12). Import perezoso; el núcleo no importafastapi.
3. b2g gui — nuevo subcomando (19º)¶
El CLI tiene 18 subcomandos hoy (el 18º,
thesaurus, lo sumó el ADR 0031/#88 el 2026-06-18);b2g guisería el 19º.
Levanta uvicorn sobre la API + sirve los assets pre-build del frontend + abre el browser. Es el adaptador de "arranque local" de la GUI.
4. Frontend — frontend/ (monorepo)¶
SPA (Vite/TS) en frontend/; su build se vendorea a src/bib2graph/gui/static/ y va al wheel
(la GUI funciona sin Node instalado, B.1). El prototipo app/src/ es throwaway (referencia UX); su
cliente TS (src/client/) es la semilla del cliente real contra el contrato.
5. Empaquetado y extras¶
- Extra
[gui]=fastapi+uvicorn(ADR 0005), import perezoso. Cierra la deuda actual (instalados a mano, no declarados). - El wheel incluye el frontend buildeado →
b2g guifunciona sin Node. - Job de CI de frontend (lint/test/build JS) + build del frontend en el release (B.3).
6. Operaciones largas y lock (v1)¶
seed/enrich/build bloquean (red, Louvain) y el store es single-writer (ADR 0019). En v1:
ejecución síncrona + lock global serializado en la API (una operación de escritura a la vez).
Jobs async/SSE de progreso y reabrir la concurrencia (0019) quedan diferidos — se evalúan si la UX
lo exige, no antes.
7. Mapeo código→HTTP¶
El código del contrato (0–5) se traduce a HTTP en el adaptador API: 0→200; 1 (uso)→400; 2
(datos)→422; 3 (dependencia)→501; 4 (red)→502; 5 (store bloqueado/corrupto)→409/503. El envelope
viaja igual en el body (la SPA lee error.code, no depende del status).
Consecuencias¶
- (+) Una sola verdad de orquestación y de contrato. CLI y API comparten
service/; se elimina el drift del envelope (no másapp/server/errors.pyduplicado). - (+)
clivuelve a ser hoja: ya no es importado por la API; ambos dependen deservice/. - (+) La SPA obtiene la lectura que necesita (
scent/network/search) sin forzar subcomandos artificiales ni granularidad imperativa de CLI (riesgo de producto de Nota 12, punto 2). - (+) Empaquetado simple para el usuario semi-técnico (0027):
uv sync --extra gui+b2g gui, sin Node. - (−) Refactor de fondo: subir el contrato (envelope/errores/mapeo) de
cli/aservice/y re-cablear los subcomandos como adaptadores. Toca muchos archivos y sus tests (aunque el contrato externo no cambie). Es el costo central de hacerlo bien ahora en vez de tarde. - (−) Nueva superficie:
service/,api/,gui/static/,frontend/+ el subcomandob2g gui, todos a construir y testear. - (−) CI más pesado: job JS nuevo + build del frontend en el release (B.3).
- (−) Mapeo código→HTTP a mantener y lock global que serializa escrituras (latencia aceptada en v1; jobs async si molesta).
- (−) Versionado: el envelope
schema="1"es compartido, pero los payloads de CLI (agente-native, ADR 0021) y de la SPA pueden evolucionar a ritmos distintos; se versiona el payload por endpoint si divergen, manteniendo el envelope estable.