Hoja de ruta — Integración BOE (Datos Abiertos) en MultiFactu
Hoja de ruta — Integración BOE (Datos Abiertos) en MultiFactu
Estado (2026-07-15): módulo IMPLEMENTADO y en producción (capas
api→application→domain←infrastructure), scope realread:boe(noboe:read), con e2e BOE 22/22 y unit BOE 246. El texto de planificación de abajo es histórico; no todo refleja la implementación final. Novedades de esta fecha (4.ª pasada delAUDIT.mdde eve):
- Rate limiting saliente IMPLEMENTADO (antes: configurado pero sin consumir).
application/services/boe-rate-limiter.service.ts(espaciador virtual scheduling, en proceso), cableado por intento enboe-http.client.adapter.ts(la espera cuenta contra el presupuesto), registrado enboe.module.ts, con tests unit. ConsumerateLimit.perSecond(boeRatePerSecond, default 5).- Warm-up del snapshot al arranque (
OnApplicationBootstrapenboe-snapshot-refresh.scheduler.ts, best-effort) para evitar el cold-start de la primera consulta a normas clave.- Pendiente (optimización, no defecto): caché HTTP condicional (ETag/304) y circuit breaker/rate-limit compartidos en Redis para multi-réplica (hoy en proceso, providers intercambiables). El caché por TTL + el rate-limit ya garantizan buena ciudadanía con
boe.es.Rama:
BOE· Objetivo: integración modular, escalable y profesional con calificación 10/10. Fuente oficial: https://www.boe.es/datosabiertos/api/api.php Especificaciones:docs-official/BOE/*.md· Catálogo de endpoints:src/modules/boe/boe-api-endpoints.yml
0. Estado de implementación (rama BOE)
| Fase | Alcance | Estado |
|---|---|---|
| P0 | Cimientos: módulo en capas, BoeConfigService (Zod), value objects (IdentificadorNorma, FechaBoe, IdBloque), constantes, puerto de transporte, wiring en AppModule/config/env. |
✅ Hecho |
| P1 | Transporte HTTP resiliente (BoeHttpClientAdapter: timeout/budget, retry+jitter, circuit breaker), parser XML con defensa XXE, primitivas de envelope (XML↔JSON). |
✅ Hecho |
| P2 | Gateway (decode + normalize + mapeo de errores RFC 7807), normalizadores por recurso con invariante XML≡JSON, builder de búsqueda, caché por capas y BoeReadService. |
✅ Hecho |
| P3 | Superficie REST (controladores finos + pipes + DTO + mapper), OpenAPI (docs/openapi/boe.json, baseline sin breaking), auth por scope boe:read. |
✅ Hecho |
| P5 | Endurecimiento: mutation testing (L03, stryker.boe.conf.json), property-based ampliado, defensa de entrada. |
🚧 En curso |
| P6 | Persistencia/búsqueda local (Prisma boe). |
⬜ Opcional/pendiente |
| P7 | Métricas Prometheus, runbook, rendimiento (microbench/k6). | ⬜ Pendiente |
Cobertura de tests entregada: unit (value objects, parsers/normalizadores, gateway, builder, caché, read service, pipes, mapper, config), property-based (L08), contrato OpenAPI (L05), y e2e REST (test/boe.e2e-spec.ts). Gate del módulo: pnpm boe:quality:gate.
Superficie expuesta a Eve (REST + OpenAPI): /boe/sumario/:fecha, /borme/sumario/:fecha, /boe/legislacion-consolidada (búsqueda), /boe/legislacion-consolidada/id/:id (+ /metadatos, /analisis, /indice), /boe/datos-auxiliares/:tabla.
1. Resumen ejecutivo
El BOE publica una API REST de datos abiertos, de solo lectura (GET sobre https, negociación por cabecera Accept, envelope response{status,data}) con cuatro familias funcionales:
- Sumario del BOE — sumario diario por fecha.
- Sumario del BORME — sumario diario del Registro Mercantil por fecha.
- Legislación Consolidada — lista/búsqueda de normas y obtención de norma completa o partes (metadatos, análisis, ELI, texto, índice y bloques).
- Datos auxiliares — vocabularios controlados (materias, ámbitos, estados de consolidación, departamentos, rangos, relaciones anteriores/posteriores).
17 endpoints upstream en total (ver §3 y el YAML canónico).
Esta integración es read-only y sin credenciales, lo que la diferencia del módulo aeat-modelos (que es transaccional, con mTLS, idempotencia y máquina de estados). Por tanto, el reto de calidad no está en la criticidad transaccional, sino en:
- Robustez de parsing dual XML/JSON sobre estructuras heterogéneas y profundas (con divergencias reales entre XML y JSON ya verificadas — ver §6.1).
- Resiliencia y buena ciudadanía frente a
boe.es(timeouts, retry con backoff+jitter, circuit breaker, presupuesto de petición, rate limiting saliente y caché agresiva — los datos cambian poco y los sumarios son inmutables una vez publicados). - Un contrato estable para Eve expuesto como REST + OpenAPI (ver §1.2 y §11). GraphQL y MCP quedan fuera de alcance (diferidos).
- Cobertura de tests en todas las capas que el repositorio ya practica (unit, integración con testcontainers, e2e, contrato OpenAPI, mutación con Stryker, property-based con fast-check, y rendimiento con tinybench/k6).
Esta hoja de ruta replica deliberadamente la arquitectura en capas y los patrones de resiliencia de src/modules/aeat-modelos, adaptados a un caso de uso de lectura/ingesta.
1.1 Propósito de negocio
MultiFactu es un backend fiscal. El BOE aporta la capa normativa de referencia:
- Resolver y versionar la legislación consolidada que respalda los cálculos fiscales (encaja con
normativeSourceVersion/policyVersionya presentes enaeat_modelos_submission). - Servir de base de datos normativa para el modo agéntico de Eve (ver §1.2): legislación y sumarios verificables y citables (
identificador+url_eli/url_html_consolidada). - Vigilar publicaciones (sumarios BOE/BORME) relevantes para clientes (cambios normativos, anuncios mercantiles).
1.2 Consumidor: Eve (decisión de superficie)
El consumidor objetivo es Eve (github.com/ADP-DIGITEK/eve.multifactu.com), descrito como “Agent layer for app.multifactu.com”. Hechos verificados en su repositorio:
- Construido con el framework Eve de Vercel (agentes durables); modelo
anthropic/claude-sonnet-4.6vía Vercel AI Gateway. - Sus capacidades son tools en TypeScript (
agent/tools/*.ts) definidas condefineTool(deeve/tools), coninputSchemade zod y unasync execute(). - Dependencias:
eve,ai(Vercel AI SDK),zod,@vercel/connect. No hay cliente MCP ni GraphQL. - Las tools sobre datos reales “se conectarán progresivamente” — es decir, su
execute()haráfetcha un endpoint HTTP.
Conclusión (Opción A): Eve consume HTTP, no MCP ni GraphQL. Por tanto este módulo expone REST + OpenAPI:
- Es el substrato universal: cualquier
execute()de Eve puede llamarlo confetch. - El
inputSchemazod de cada tool de Eve mapea 1:1 con el DTO/esquema OpenAPI de nuestro endpoint (incluso se puede generar cliente desde el spec). - MCP/GraphQL serían peso muerto para este consumidor. Si en el futuro Eve adoptara un runtime MCP-nativo, las tools MCP de este repo son fachadas finas (
src/mcp/tools/*→gqlFetch): añadirlas costaría horas, sin rediseño. Se documentan como superficies diferidas (§11.3), no se construyen ahora.
Principio que protege la reversibilidad: la lógica vive en application/domain (puertos); las superficies son adaptadores finos encima. Mantener esa disciplina deja la puerta abierta a MCP/GraphQL a coste casi nulo, sin construirlos en especulativo.
2. Rúbrica 10/10 (cómo se mide el éxito)
| # | Dimensión | Peso | Criterio de “10” |
|---|---|---|---|
| 1 | Arquitectura en capas | 10% | api → application → domain ← infrastructure limpio; pasa pnpm arch:check (dependency-cruiser) sin violaciones. |
| 2 | Cobertura funcional | 10% | Los 17 endpoints upstream cubiertos en el motor (application/infra) y expuestos a Eve mediante 2-3 contratos REST con forma de intención. |
| 3 | Parsing y normalización | 10% | XML y JSON producen el mismo modelo de dominio normalizado, resolviendo las divergencias reales (data[] envuelto, mapas aux, errores XML); fixtures golden de respuestas reales. |
| 4 | Resiliencia saliente | 10% | Timeout + retry(backoff+jitter) + circuit breaker + request budget + rate limit + caché con ETag/If-None-Match. |
| 5 | Caché y escalabilidad | 10% | Caché por capas (memoria/Redis), claves deterministas, invalidación e inmutabilidad de sumarios; sin llamadas redundantes. |
| 6 | Contrato para Eve (REST + OpenAPI) | 10% | Endpoints REST documentados en OpenAPI, con auth/scopes, negociación de formato y baseline de contrato sin drift. GraphQL/MCP diferidos (no puntúan). |
| 7 | Persistencia | 5% | Schema boe en Prisma con multitenancy donde aplique, índices correctos, migración reproducible. |
| 8 | Observabilidad | 5% | Métricas Prometheus por endpoint, logs estructurados, tracing OTel, correlationId. |
| 9 | Seguridad y robustez | 5% | Validación de entrada, defensa XXE en parser XML, límites de tamaño/profundidad, manejo de errores RFC 7807. |
| 10 | Testing multicapa | 15% | Unit + integración (testcontainers) + e2e + contrato OpenAPI + mutación (Stryker ≥ break) + property-based + rendimiento. Cobertura global ≥ 85/80 y ≥ 95/90 en parsers/validadores/mappers. |
El “10/10” se alcanza cuando todas las dimensiones están en verde y el quality gate del módulo (boe:quality:gate, §15) pasa en CI.
3. Alcance — endpoints upstream
Catálogo canónico y machine-readable: src/modules/boe/boe-api-endpoints.yml.
| Familia | Endpoint | Formatos | Notas |
|---|---|---|---|
| Sumario BOE | GET /boe/sumario/{fecha} |
JSON, XML | fecha=AAAAMMDD |
| Sumario BORME | GET /borme/sumario/{fecha} |
JSON, XML | nodo <apartado> solo en sección C |
| Consolidada | GET /legislacion-consolidada |
JSON, XML | búsqueda query+range+sort, from/to/offset/limit |
| Consolidada | GET /legislacion-consolidada/id/{id} |
XML | norma completa |
| Consolidada | GET /legislacion-consolidada/id/{id}/metadatos |
JSON, XML | |
| Consolidada | GET /legislacion-consolidada/id/{id}/analisis |
JSON, XML | materias/notas/referencias |
| Consolidada | GET /legislacion-consolidada/id/{id}/metadata-eli |
XML | |
| Consolidada | GET /legislacion-consolidada/id/{id}/texto |
XML | puede ser muy grande |
| Consolidada | GET /legislacion-consolidada/id/{id}/texto/indice |
JSON, XML | |
| Consolidada | GET /legislacion-consolidada/id/{id}/texto/bloque/{id_bloque} |
XML | |
| Aux. | GET /datos-auxiliares/materias |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/ambitos |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/estados-consolidacion |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/departamentos |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/rangos |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/relaciones-anteriores |
JSON, XML | cache larga |
| Aux. | GET /datos-auxiliares/relaciones-posteriores |
JSON, XML | cache larga |
4. Principios de arquitectura
Se siguen las reglas de CLAUDE.md raíz y de src/modules/CLAUDE.md:
- Módulo en capas (dominio de contrato externo):
api → application → domain ← infrastructure. - Controllers/resolvers/tools finos: la lógica vive en
application/domain, no ensrc/graphql,src/mcpni controladores. - Puertos e inversión de dependencias: la
applicationdepende de interfaces (domain/ports), implementadas eninfrastructuree inyectadas por token en el.module.ts(igual queAEAT_*_TRANSPORT_PORT). - Config validada: un
BoeConfigServicelee deConfigServicey valida con Zod (espejo deaeat-modelos.config.tsysrc/core/config/env.validation.ts). - Error envelope global: se reutiliza
AllExceptionsFilter(RFC 7807). Los errores de dominio extiendenDomainException. - Sin acoplar al runtime de docs:
docs-official/BOEy el YAML no se compilan ni se sirven.
5. Estructura de carpetas propuesta
src/modules/boe/
├── boe-api-endpoints.yml # catálogo canónico (ya creado)
├── boe.module.ts # wiring: providers, controllers, puertos
├── boe.config.ts # BoeConfigService (Zod) + .spec.ts
│
├── api/
│ ├── controllers/
│ │ ├── boe-sumario.controller.ts # GET /boe/sumario/:fecha, /borme/sumario/:fecha
│ │ ├── boe-legislacion.controller.ts # lista + norma + subrecursos
│ │ └── boe-datos-auxiliares.controller.ts
│ ├── dto/
│ │ ├── sumario.dto.ts # FechaParamDto (AAAAMMDD)
│ │ ├── legislacion-query.dto.ts # from/to/offset/limit/query (+ validadores)
│ │ └── norma-id.dto.ts # IdNormaParamDto, IdBloqueParamDto
│ └── mappers/
│ └── domain-to-dto.mapper.ts # dominio -> respuesta pública estable
│
├── application/
│ ├── use-cases/
│ │ ├── get-sumario-boe.use-case.ts
│ │ ├── get-sumario-borme.use-case.ts
│ │ ├── list-normas.use-case.ts
│ │ ├── get-norma.use-case.ts # completa/metadatos/analisis/eli
│ │ ├── get-texto.use-case.ts # texto/indice/bloque
│ │ └── get-datos-auxiliares.use-case.ts
│ └── services/
│ ├── boe-cache.service.ts # claves deterministas + TTL por recurso
│ ├── boe-search-query.builder.ts # serializa query_string/range/sort -> JSON
│ ├── boe-circuit-breaker.service.ts # (reutilizar patrón aeat) por familia
│ ├── boe-rate-limit.service.ts # admisión saliente (memory|redis)
│ └── boe-runtime-metrics.service.ts # Prometheus por endpoint
│
├── domain/
│ ├── constants/boe.constants.ts # secciones, tipos de bloque, códigos retorno
│ ├── ports/
│ │ ├── boe-transport.port.ts # BOE_TRANSPORT_PORT (fetch crudo)
│ │ ├── boe-cache.port.ts # BOE_CACHE_PORT
│ │ └── boe-repository.port.ts # BOE_REPOSITORY_PORT (mirror/índice opcional)
│ ├── value-objects/
│ │ ├── identificador-norma.vo.ts # parse/validate BOE-A-AAAA-N
│ │ ├── fecha-boe.vo.ts # AAAAMMDD <-> Date
│ │ └── id-bloque.vo.ts
│ └── types/boe.types.ts # Sumario, Norma, Metadatos, Analisis, Texto, Bloque...
│
└── infrastructure/
├── adapters/
│ └── boe-http.client.adapter.ts # implementa BOE_TRANSPORT_PORT (@nestjs/axios)
├── parsers/
│ ├── boe-xml.parser.ts # fast-xml-parser + defensa XXE
│ ├── boe-json.parser.ts
│ └── normalizers/ # XML|JSON -> modelo de dominio único
├── cache/
│ └── boe-cache.adapter.ts # cache-manager + @keyv/redis (ya en deps)
├── db/
│ └── boe.repository.ts # Prisma (mirror/índice, opcional fase 6)
└── schedulers/
└── boe-aux-refresh.scheduler.ts # refresco de datos auxiliares (@nestjs/schedule)
6. Modelo de dominio
Tipos en domain/types/boe.types.ts y value objects con validación propia:
IdentificadorNorma— valida^[A-Z]{3}-[A-Z]-\d{4}-\d{1,5}$; normaliza mayúsculas; exponepublicacion(BOE/BORME) yanio.FechaBoe—AAAAMMDD⇆DateUTC; rechaza fechas imposibles; helper parafecha_actualizacionAAAAMMDDTHHmmSSZ.IdBloque— identificador de bloque (pr,a1,dd,df,fi,an…).- Modelos normalizados:
SumarioBoe,SumarioBorme,NormaConsolidada,MetadatosNorma,AnalisisNorma(materias[],notas[],referencias{anteriores[],posteriores[]}),TextoConsolidado(bloque[].version[]),IndiceTexto,DatosAuxiliares.
Invariante clave (testeable): parsear el mismo recurso en XML y en JSON debe producir un modelo de dominio idéntico (normalización canónica de item único vs array, atributos @codigo, texto, etc.).
6.1 Divergencias XML vs JSON (verificadas en vivo el 2026-06-23)
La documentación oficial describe la estructura XML; la serialización JSON difiere. Comprobado con curl contra https://www.boe.es y reflejado en boe-api-endpoints.yml (bloque json_serialization y verification). El normalizador debe absorber estas diferencias para garantizar el invariante anterior:
| Recurso | XML | JSON (real) | Normalización |
|---|---|---|---|
metadatos, analisis, texto/indice |
data.<nodo> |
data es array de 1 elemento (data[0]) |
desenvolver data[0] |
| Lista de consolidada | data.item[] |
data es array de items (sin item) |
usar data[] |
| Datos auxiliares | data con nodos |
data es mapa { "<codigo>": "<descripcion>" } |
mapear a [{codigo, texto}] |
| Sumarios | data.sumario... |
igual; item/diario únicos llegan como objeto |
coaccionar a array |
Recursos XML-only (norma completa, metadata-eli, texto, texto/bloque) |
XML | no hay JSON: Accept: application/json → 400 “No soportado ningún mime type” |
pedir Accept: application/xml |
| Respuestas de error | XML | el envelope de error puede venir como XML aunque se pida JSON (observado en 404) | el adapter debe detectar y parsear envelope XML en errores |
Estas reglas son la base de los tests de parsing (L1) y del invariante con property-based (L8).
7. Capa infrastructure
7.1 Adapter HTTP (boe-http.client.adapter.ts)
Replica el patrón de resiliencia de consulta-ext.client.adapter.ts (aeat-modelos), pero para GET:
- Cliente:
@nestjs/axios(HttpService),validateStatus: () => true,responseType: 'text'. - Cabecera
Acceptsegún formato solicitado; por defecto JSON donde esté disponible, XML cuando sea el único formato (norma completa, texto, ELI, bloque). - Timeout por intento + request budget total.
- Retry exponencial con jitter solo en
429/5xx/errores de red. - Circuit breaker por familia de endpoint (sumario/legislación/auxiliares).
- Rate limiting saliente (buena ciudadanía con
boe.es): límite configurable de req/seg. - Caché HTTP condicional: almacenar
ETag/Last-Modified; reenviarIf-None-Match/If-Modified-Since; tratar304como hit. - Métricas por intento (
endpoint,statusCode,latencyMs,cache=hit|miss|revalidated).
7.2 Parsing dual y defensa
fast-xml-parser(ya en deps) conignoreAttributes:false,attributeNamePrefixconsistente; defensa XXE (sin DTD/entidades externas) y límites de profundidad/tamaño.- Para validación estructural:
ajv(en deps) con JSON Schema propio por recurso (el BOE no publica XSD para estas APIs). El XSD solo se usaría si BOE lo publicara; mientras tanto, JSON Schema canónico. - Normalizadores: unifican peculiaridades (un solo
itemllega como objeto, varios como array; coerción deszBytes/pagina_*a number; fechas aFechaBoe).
7.3 Caché (boe-cache.adapter.ts)
cache-manager+@keyv/redis(ya en deps).- Política por recurso:
- Sumarios (BOE/BORME) de fechas pasadas → inmutables, TTL muy largo / permanente.
- Datos auxiliares → TTL ≥ 24 h + refresco programado (
boe-aux-refresh.scheduler.ts). - Lista/búsqueda de consolidada → TTL corto (minutos).
- Norma/metadatos/texto → TTL medio + revalidación por
ETag.
- Claves deterministas:
boe:{familia}:{recurso}:{args-normalizados}:{formato}(testeable; ver property-based §14).
8. Capa application
- Use-cases finos que orquestan: validar entrada → consultar caché → (miss) adapter HTTP → parsear/normalizar → cachear → mapear.
boe-search-query.builder.ts: construye el JSON dequery(campos permitidos,rangesolo en fechas,sort) con validación estricta de campos para evitar400 Search error.get-texto.use-case.ts: soportatextocompleto,indiceybloque; estrategia recomendada para front/IA = índice + bloques bajo demanda (evita descargar textos enormes).- Servicios transversales (circuit breaker, rate limit, métricas) compartidos por los use-cases.
9. Capa api (REST)
- Controllers finos con DTOs
class-validator:FechaParamDto(@Matches(/^\d{8}$/)+ validación semántica).IdNormaParamDto,IdBloqueParamDto.LegislacionQueryDto(from,to,offset,limit,query) con validadores y saneo.
- Auth/scopes: rutas bajo
RestAuthGuardglobal; scopesboe:read(lectura) declarados con@RequireScopes. Decidir en P0 si parte es público (@PublicRoute) o todo autenticado (recomendado: autenticado + rate limit de edge). - Negociación de formato: query
?format=json|xmlo passthrough deAccept; por defecto JSON normalizado del producto. - Errores:
AllExceptionsFilter→ RFC 7807; mapear404upstream a 404 de dominio y400upstream a400de validación. - Swagger: decoradores
@nestjs/swaggercompletos para alimentar OpenAPI.
Base de rutas propuesta: /boe/sumario, /borme/sumario, /boe/legislacion-consolidada/..., /boe/datos-auxiliares/....
10. Persistencia (Prisma) — opcional/fase avanzada
Schema dedicado boe en packages/prisma/schema.prisma (patrón multi-schema ya usado).
boe_http_cache(si se prefiere cache durable a Redis):cache_key @id,etag,last_modified,payload Json,format,fetched_at,expires_at.boe_norma_index(mirror/índice de búsqueda local, fase 6):identificador @id,titulo,rango_codigo,departamento_codigo,fecha_publicacion,fecha_actualizacion,estado_consolidacion_codigo,vigencia_agotada,organization_id?(si hay listas de seguimiento por organización), índices compuestos(@@index([fecha_actualizacion])),(@@index([rango_codigo, fecha_publicacion])).- Timestamps
@db.Timestamptz(6); naming snake_case; migración reproducible ypnpm prisma:validate.
La integración funciona end-to-end sin Prisma (caché Redis + upstream). La persistencia se añade para búsqueda local, watchlists y resiliencia offline.
11. Superficies públicas
Decisión (§1.2): la única superficie activa es REST + OpenAPI, porque Eve consume HTTP. GraphQL y MCP se documentan como diferidos, no se construyen.
11.1 REST — contratos orientados a Eve (superficie ACTIVA)
El motor (application/infrastructure) cubre los 17 endpoints upstream, pero la superficie pública expuesta a Eve se diseña con forma de intención, pequeña y estable, para que cada tool de Eve la mapee 1:1. Contratos propuestos (a refinar con casos de uso reales de Eve):
| Contrato REST | Tool de Eve equivalente | Devuelve |
|---|---|---|
GET /boe/legislacion-consolidada (búsqueda) |
boe_buscar_legislacion |
lista con metadatos resumidos + identificador + enlaces citables |
GET /boe/legislacion-consolidada/{id} (metadatos + análisis + índice) |
boe_obtener_norma |
norma “agregada” lista para citar, sin el texto enorme |
GET /boe/legislacion-consolidada/{id}/bloque/{idBloque} |
boe_obtener_bloque |
un bloque concreto del texto, bajo demanda |
GET /boe/sumario/{fecha} y GET /borme/sumario/{fecha} |
boe_sumario / borme_sumario |
sumario del día |
GET /boe/datos-auxiliares/{tabla} |
(interno / cache) | vocabularios (materias, rangos, etc.) |
- Controllers finos + DTOs
class-validator; respuesta JSON normalizada del producto (no el JSON crudo del BOE: ya desenvuelto, conitem/diariocoaccionados a array y aux como[{codigo,texto}]). - Auth/scopes:
RestAuthGuardglobal +@RequireScopes('boe:read'). - OpenAPI: decoradores
@nestjs/swaggercompletos para que el spec sirva de contrato a Eve (y permita generar cliente/typings para sus tools). - Entregable para Eve: además del spec, una nota de integración (
docs-official/BOE/README.md) con los esquemas zod sugeridos paradefineTool, derivados del OpenAPI.
11.2 OpenAPI (contrato y no-drift)
pnpm openapi:export:docsgeneradocs/openapi/boe.json(split por tagboe).- Añadir
boeascripts/split-openapi.tsy refrescar baseline (pnpm openapi:baseline:refresh). - Gate de drift:
pnpm openapi:diffsin breaking changes no intencionados.
11.3 Superficies diferidas (NO en alcance) — GraphQL y MCP
- No se construyen porque Eve no las consume (sin cliente MCP/GraphQL; ver §1.2).
- Quedan disponibles a coste bajo si cambian los requisitos:
- GraphQL:
BoeGraphqlModule+BoeQueryResolver(solo queries;complexitypor campo;@RequireScopes('boe:read')). - MCP:
src/mcp/tools/boe.tscon tools finas que llaman al GraphQL/REST propio (patrónverifactu.ts), schemas ensrc/mcp/schemas.ts.
- GraphQL:
- Como la lógica vive en
application/domain, añadirlas serían adaptadores finos sin tocar el núcleo.
12. Configuración (env vars)
BoeConfigService (Zod), variables con prefijo BOE_:
| Variable | Default | Uso |
|---|---|---|
boeEnabled |
false |
habilita el módulo |
boeBaseUrl |
https://www.boe.es |
base upstream |
boeTimeoutMs |
15000 |
timeout por intento |
boeRetries |
2 |
reintentos |
boeRetryBaseDelayMs |
400 |
backoff base |
boeRequestBudgetMs |
auto | presupuesto total |
boeCircuitBreakerThreshold |
5 |
fallos para abrir |
boeCircuitBreakerCooldownMs |
30000 |
cooldown |
boeRatePerSecond |
5 |
rate limit saliente |
boeCacheBackend |
auto |
memory/redis/auto |
boeCacheTtlAuxMs |
86400000 |
TTL datos auxiliares |
boeCacheTtlListMs |
300000 |
TTL listas/búsqueda |
boeCacheTtlNormaMs |
3600000 |
TTL norma/metadatos/texto |
Validación: error de arranque si boeEnabled=true y falta boeBaseUrl. Añadir el bloque al env.validation.ts y a configuration.ts.
13. Observabilidad
- Métricas Prometheus (
prom-client, ya en deps): contador/histograma porendpoint,format,outcome,cache. Exponer en el endpoint de métricas existente. - Logs estructurados (
nestjs-pino) concorrelationId(CLS) ya disponible. - Tracing OTel (auto-instrumentación ya configurada) — spans por familia y por intento HTTP.
- SLO sugerido: p95 < 500 ms con caché caliente; tasa de error upstream < 1%.
14. Estrategia de testing por capa (núcleo del 10/10)
Se replican exactamente las capas y convenciones del repo (numeración L##, fixtures, testcontainers, Stryker, fast-check, tinybench/k6). Cobertura global ya exigida: branches 80 / functions 80 / lines 85 / statements 85; 95/90 en archivos críticos vía coverageThreshold por archivo en package.json.
L1 — Unit (*.spec.ts, junto al código)
- Value objects:
IdentificadorNorma,FechaBoe,IdBloque— validación, normalización, casos límite. - Parsers/normalizadores: XML→dominio y JSON→dominio, incluyendo el caso
itemúnico vs array, atributos, coerción numérica. boe-search-query.builder: serialización dequery_string/range/sort; rechazo de campos no permitidos.boe-cache.service: derivación de claves y selección de TTL por recurso.- Use-cases: con
BOE_TRANSPORT_PORTyBOE_CACHE_PORTmockeados (jest.fn), camino hit/miss/revalidate, propagación de 404/400. - Mappers dominio→DTO.
- Objetivo de cobertura: 95/90 en parsers, value objects, builder y mappers.
L2 — Integración (*.integration.spec.ts, testcontainers)
- Redis (
test/support/testcontainers/redis.ts):boe-cache.adapterreal contra Redis efímero (hit/miss/TTL/revalidación). GatingAGENTS_REQUIRE_REDIS/CI. - Postgres (
test/support/testcontainers/postgres.ts,DB_INTEGRATION=1):boe.repository(si se implementa P6) con cadena de migraciones Prisma (startMigratedTestDb()), round-trip de índice/cache. - Adapter HTTP contra un servidor BOE simulado local (fixtures golden) para validar retry/circuit-breaker/budget/ETag de forma determinista.
e2e (test/boe.e2e-spec.ts, supertest) — solo REST
- App Nest con
BOE_TRANSPORT_PORTmockeado (sin red real), igual quetest/verifactu.e2e-spec.tsmockea puertos. - Cubre los contratos REST expuestos a Eve: 200 con JSON normalizado, 404 (norma inexistente), 400 (fecha/id/búsqueda inválidos), 403 (método no permitido), paginación de lista.
- Caso clave (consumo Eve): simular un
fetchcomo el delexecute()de una tool, validando que el cuerpo se ajusta alinputSchema/salida esperada por Eve.
L5 — Contrato OpenAPI (test/contract/ + jest-openapi)
- Generar OpenAPI, añadir
boeal split, refrescartest/contract/openapi.baseline.json. expect(response).toSatisfyApiSpec()en e2e (helpertest/openapi-conformance.helper.ts).pnpm openapi:diffen CI sin breaking changes no intencionados.- Test de presencia de rutas (estilo
agents.openapi-contract.spec.ts): verificar que los contratos REST expuestos a Eve existen en el spec generado.
L3 — Mutación (Stryker, stryker.conf.json)
Añadir a mutate los archivos deterministas y críticos:
domain/value-objects/*.ts(identificador, fecha, bloque).infrastructure/parsers/**ynormalizers/**.application/services/boe-search-query.builder.tsyboe-cache.service.ts.- Umbral: respetar
breakactual (≥ 80) sin regresión; objetivohigh85.
L8 — Property-based (fast-check, *.property.spec.ts)
FechaBoe: round-tripAAAAMMDD ⇆ Date; rechazo de fechas imposibles.IdentificadorNorma: round-trip parse/format; idempotencia de normalización.- Equivalencia XML/JSON: para un mismo recurso generado, ambos parsers producen el mismo dominio (invariante central de §6).
boe-cachekey: estabilidad y unicidad ante permutación de parámetros.boe-search-query.builder: la serialización nunca produce JSON inválido.
L10/L11 — Rendimiento (scripts/performance/)
- Microbench (tinybench): hotpath de parsing de texto consolidado grande (con imágenes base64) y de búsqueda; baseline con tolerancia (±10%) en
performance/bench/baselines/. - Carga (k6): escenarios
smoke/nightlysobre endpoints de lectura con caché caliente; thresholds p95/p99 y error-rate.
Fixtures golden
test/fixtures/boe/con respuestas reales (recortadas) en XML y JSON por cada familia: sumario BOE, sumario BORME, lista, metadatos, análisis, texto (con bloque e imagen base64), índice, y los 7 datos auxiliares. Son la base de L1/L2/e2e/L5/L8.
15. Quality gate del módulo
Añadir a package.json (espejo de ticketbai:quality:gate):
"boe:test:unit": "jest --testPathPatterns=src/modules/boe",
"boe:test:e2e": "jest --config ./test/jest-e2e.json --testPathPatterns=boe",
"boe:openapi:contract": "jest --runInBand --testPathPatterns=src/modules/boe/.*openapi-contract",
"boe:quality:gate": "pnpm typecheck && ultracite check src/modules/boe && pnpm boe:test:unit && pnpm boe:test:e2e && pnpm openapi:diff"
Sin
boe:test:mcpni gates de GraphQL: esas superficies están diferidas (§11.3).
arch:check (dependency-cruiser) debe pasar para el subárbol src/modules/boe.
16. Plan de fases
Cada fase es un conjunto de commits sobre esta rama
BOE, con su propio gate verde antes de continuar. Las fases P0–P4 entregan una integración productiva consumible por Eve; P5–P7 elevan a 10/10.
P0 — Cimientos del módulo (scaffolding)
boe.module.ts,boe.config.ts(+Zod, env vars), registro enapp.module.ts.domain/(tipos, value objects, ports, constants).- Bloque de config en
configuration.ts+env.validation.ts. - Tests: L1 de value objects;
boe.config.spec.ts;boe.module.contract.spec.ts. - DoD:
pnpm typecheck && pnpm arch:check && boe:test:unitverde.
P1 — Adapter HTTP + parsing dual
boe-http.client.adapter.ts(resiliencia completa),boe-xml.parser.ts,boe-json.parser.ts, normalizadores.- Fixtures golden iniciales.
- Tests: L1 parsers/normalizadores; L8 equivalencia XML/JSON; L2 adapter contra servidor simulado.
- DoD: parsing de las 4 familias verde; invariante XML≡JSON probado.
P2 — Use-cases + caché
- Use-cases de sumarios, lista/búsqueda, norma y subrecursos, datos auxiliares.
boe-cache.service+boe-cache.adapter(Redis) +boe-search-query.builder.boe-aux-refresh.scheduler.- Tests: L1 use-cases/builder/cache; L2 caché contra Redis (testcontainers).
- DoD: hit/miss/revalidación probados; búsqueda construida y validada.
P3 — Superficie REST + OpenAPI
- Controllers, DTOs, mappers, auth/scopes, Swagger; respuesta JSON normalizada del producto.
- Tests: e2e (200/400/404/paginación); L5 contrato (baseline + drift + presencia de rutas).
- DoD:
boe:test:e2eypnpm openapi:diffverdes; baseline actualizado.
P4 — Contratos orientados a Eve + handoff
- Diseñar/afinar los 2-3 contratos REST con forma de intención (§11.1) según los casos de uso reales de Eve.
- Generar typings/cliente desde el OpenAPI y documentar en
docs-official/BOE/README.mdlosinputSchemazod sugeridos paradefineTool. - (Opcional) Spike de una tool en
eve.multifactu.com(agent/tools/boe_buscar_legislacion.ts) que hagafetchal contrato, como validación end-to-end del consumo. - Tests: e2e del contrato Eve-facing (cuerpo conforme al esquema esperado por la tool); contrato OpenAPI estable.
- DoD: Eve puede consumir el contrato con un
fetchsimple; sin GraphQL ni MCP.
P5 — Endurecimiento (mutación + property-based + seguridad)
- Añadir archivos críticos a
stryker.conf.json; subir property-based. - Defensa XXE, límites de tamaño/profundidad de XML, fuzz de entradas.
- Tests: L3 (Stryker ≥ break sin regresión); L8 ampliado.
- DoD: mutation score del subárbol ≥ umbral; sin findings de seguridad.
P6 — Persistencia y búsqueda local (opcional, escalabilidad)
- Schema Prisma
boe, migración,boe.repository, índice de búsqueda local, watchlists por organización. - Tests: L2 Postgres (testcontainers) con cobertura 95/90 del repositorio (patrón
aeat-modelos.repository). - DoD: round-trip + búsqueda local probados; migración reproducible.
P7 — Rendimiento, observabilidad y operación
- Métricas Prometheus, dashboards/SLO, microbench + baselines, k6.
- Runbook (
docs/boe-runbooks.md) y README de contrato (docs-official/BOE/README.mdya creado, ampliar con contrato backend). - Tests: L10/L11 con baselines; alertas.
- DoD: SLO definido y medido;
boe:quality:gatecompleto verde en CI.
17. Riesgos y mitigaciones
| Riesgo | Impacto | Mitigación |
|---|---|---|
| Textos consolidados enormes (imágenes base64) | Memoria/latencia | Preferir indice + bloque bajo demanda; streaming; límites de tamaño. |
Heterogeneidad XML (item único vs array) |
Bugs de parsing | Normalizadores + invariante XML≡JSON con property-based. |
Caída/lentitud de boe.es |
Disponibilidad | Circuit breaker, budget, caché durable, 304 revalidation. |
| Sobrecarga al upstream | Bloqueo/ética | Rate limit saliente + caché agresiva + refresco programado de auxiliares. |
| XXE / entidades maliciosas | Seguridad | Parser sin DTD/entidades externas; límites de profundidad. |
| Drift de contrato OpenAPI | Rotura de clientes | openapi:diff en gate; baseline versionado. |
| Cambios normativos en la API BOE | Mantenimiento | Catálogo YAML como fuente única; tests de contrato sobre fixtures. |
18. Definition of Done (checklist 10/10)
- Módulo en capas;
pnpm arch:checkverde parasrc/modules/boe. - 17 endpoints upstream cubiertos en el motor; expuestos a Eve mediante 2-3 contratos REST con forma de intención.
- Parsing XML y JSON normalizado a un único modelo, resolviendo las divergencias de §6.1; invariante XML≡JSON probado.
- Resiliencia completa (timeout/retry/jitter/circuit-breaker/budget/rate-limit/ETag).
- Caché por capas con claves deterministas y políticas por recurso.
- OpenAPI generado,
boe.jsonsplit, baseline yopenapi:diffsin breaking. - Contrato consumible por Eve con
fetch;inputSchemazod documentados. GraphQL/MCP diferidos (§11.3), no requeridos. - (Opcional) Persistencia Prisma
boecon migración reproducible. - Observabilidad: métricas Prometheus, logs con correlationId, tracing OTel.
- Tests en todas las capas: L1 unit, L2 integración (Redis+Postgres testcontainers), e2e REST, L5 contrato OpenAPI, L3 mutación (Stryker), L8 property-based, L10/L11 rendimiento.
- Cobertura global ≥ 85/80 y ≥ 95/90 en parsers/value-objects/builder/mappers/repository.
-
boe:quality:gateverde en CI. - Documentación: este roadmap,
boe-api-endpoints.yml,docs-official/BOE/, runbook.
19. Referencias del repositorio (patrones a replicar)
- Módulo en capas y resiliencia HTTP:
src/modules/aeat-modelos/**(adapters, circuit breaker, config Zod, repository, schedulers). - Contrato frontend/backend de referencia:
docs-official/AEAT/README.md. - Consumidor (Eve):
github.com/ADP-DIGITEK/eve.multifactu.com(agent/tools/*.tscondefineTool+zod,agent/agent.ts,package.json). - OpenAPI:
scripts/generate-openapi-json.ts,scripts/split-openapi.ts,scripts/openapi/check-breaking.ts,test/contract/openapi.baseline.json. - Diferidos (solo si se reactivan §11.3) — GraphQL:
src/graphql/integrations/verifactu/**,src/graphql/shared/complexity.plugin.ts. MCP:src/mcp/tools/verifactu.ts,src/mcp/schemas.ts,src/mcp/shared.ts. - Prisma multi-schema:
packages/prisma/schema.prisma(aeat_modelos_submission). - Auth/errores:
src/core/auth/rest-auth.guard.ts,src/core/auth/require-scopes.decorator.ts,src/core/error/all-exception-filter.ts. - Testing: testcontainers
test/support/testcontainers/*, e2etest/verifactu.e2e-spec.ts, contratotest/openapi-conformance.helper.ts, mutaciónstryker.conf.json, property-basedsrc/modules/**/**.property.spec.ts, rendimientoscripts/performance/*.