# BITACORA - tuprotesis.com
Registro de cierres de loop, decisiones y desviaciones (reglas de oro 9/10 del master prompt).

## Loop 0 — Fundaciones y pipeline de despliegue — CERRADO
Fecha: 2026-07-05 · Entorno de construccion: contenedor Ubuntu 24, Node v22.22.2, MariaDB local (equivalente MySQL cPanel).
Checks:
1. curl /salud -> {"ok":true,"version":"0.1.0","db":"up"} ✔
2. curl /no-existe -> 404 con pagina propia ✔
3. .cpanel.yml presente con las 3 tareas exactas de la seccion 2.3 ✔
4. Remoto prod: PENDIENTE-OPERADOR (pasos 1-4 de la seccion 2.2 en cPanel). El pipeline queda documentado en docs/DEPLOY.md.
Test idempotencia migraciones: 1a pasada aplica 000_bootstrap.sql, 2a pasada "0 pendientes" ✔
Decisiones (regla 9): password de BD del entorno de desarrollo en .env local (fuera de git); en produccion la fija el operador.

## Loop 1 — DESIGN.md y sistema de diseño — CERRADO
Fecha: 2026-07-05
Checks:
1. "/" renderiza layout base con home provisional (hero + 3 caminos + newsletter placeholder) ✔
2. /admin/_styleguide muestra los 14 componentes de DESIGN.md (19 secciones h2, sin estilos rotos) ✔
3. Contraste AA calculado con scripts/check-contraste.js — 8/8 pares ✔ (mín. 5.08:1, máx. 13.95:1)
4. main.css = 10.7 KB (< 60 KB) · 0 etiquetas <style> en vistas ✔
5. 380px/1280px: auditoría CSS — grid fluida, sin anchos fijos, .tabla-envoltorio con overflow-x, imágenes max-width:100% ✔ (verificación visual en navegador pendiente-operador, anotada)
Decisiones (regla 9):
- El acento #E0703C daba 2.87:1 sobre blanco (suspende AA): desdoblado en --color-acento #B4511F (texto/CTA)
  y --color-acento-claro #E0703C (decorativo). Documentado en DESIGN.md.
- OG image por defecto en SVG; raster 1200x630 pendiente (algunas plataformas no leen SVG en OG). TODO L25.
- Composición de layout: render('layouts/base', {vista}) con include dinámico de EJS (sin dependencia de layouts).

## Loop 2 — Migración núcleo: el grafo de conocimiento — CERRADO
Fecha: 2026-07-05
Checks:
1. migrate x2: 1ª aplica 001_grafo_nucleo.sql, 2ª "0 pendientes" ✔
2. 24 tablas en information_schema (≥ 22 exigidas) ✔
3. check-graph.js: link idempotente (uq_edge), related() en ambas direcciones, relación fuera de
   vocabulario rechazada, crossQuery canónico, rollback sin residuos — 6/6 ✔
4. check-publicacion.js: publicar sin revisado_por+fecha_revision → PUBLICACION_BLOQUEADA;
   con revisión → publicado ✔
5. Admin bootstrap creado desde .env (solo si no existe) ✔ · Seeds tolerante a directorio vacío ✔
Decisiones (regla 9):
- FKs de revisado_por hacia users omitidas a propósito (validación en capa modelo, evita dependencia circular).
- bcrypt nativo compiló sin problemas en el entorno (sin fallback a bcryptjs).

## Loop 3 — Back office editorial — CERRADO
Fecha: 2026-07-05
Checks (todos vía interfaz real con curl + cookies):
1. Creada 1 entidad de cada uno de los 12 tipos en borrador (12/12 confirmadas en BD) ✔
2. Editor: botón Publicar ausente y POST → 403; revisor_medico publica y el sistema estampa
   revisado_por=3 + fecha_revision=2026-07-05 ✔
3. Aristas pertenece_a_tipo (producto→tipo) y cubre (póliza→procedimiento) con attrs JSON
   {"copago":"0 EUR","carencia_meses":10} y verificada=1, origen='editorial' ✔
4. audit_log: 17 operaciones registradas (crear, actualizar, estado, aristas) ✔
5. Login: 5 fallos → bloqueado_hasta fijado; con contraseña CORRECTA sigue rechazando con mensaje
   de bloqueo ✔ · CSRF: POST sin token → 403 ✔
Decisiones (regla 9):
- Express 5 (path-to-regexp v8): sintaxis :id(\d+) sustituida por router.param('id') con validación numérica.
- CSRF propio por sesión (csurf está deprecado y fuera de la lista de dependencias permitidas).
- Subida de imágenes multer(memoria)+sharp → variantes webp 1200/400 en /public/uploads/{tipo}/{id}/.

## Loop 4 — Motor de fichas universal — CERRADO
Fecha: 2026-07-05
Checks (con datos publicados vía admin real):
1. Patología + tipo + producto demo publicados → las 3 URLs públicas renderizan las 6 capas en orden
   (identidad, respuesta corta, contenido dual, E-E-A-T/fuentes, relaciones del grafo, acción) ✔
2. ?vista=pro sin cookie → 302 al interstitial /pro; tras aceptar → contenido_pro con barra de contexto
   y meta robots noindex ✔
3. Producto sin contenido_pro en vista pro → banner "Sin versión profesional" + fallback a contenido
   paciente (nunca vacío) ✔
4. NODE_ENV=production: demo publicada → 404 para anónimo (es_demo solo visible en dev o a staff) ✔
5. check-html.js: un solo H1, <main>/<nav>, lang=es, <title>, jerarquía H2/H3 sin saltos — 3/3 fichas ✔
Bug encontrado y corregido (regla 9):
- El handler de edición recalculaba el slug desde el nombre, y como upsertPorSlug busca por slug,
  editar el nombre de una entidad creaba una fila NUEVA en vez de actualizar. Corregido: en edición se
  conserva el slug previo salvo que el editor escriba uno distinto en el campo slug. Se archivaron
  (soft-delete) las filas duplicadas vacías que esto había generado.
Otras decisiones:
- Markdown propio (utils/markdown.js) con TOC y anclas estables: sin dependencia 'marked', escapado previo.
- Vista dual con cookie tp_pro (consentimiento) + interstitial; la vista pro siempre noindex.

## Loop 5 — Ficha de producto completa (B1-B6) + comparador — CERRADO
Fecha: 2026-07-05
Checks:
1. DemoKnee X1 (activo) renderiza los 6 bloques B1-B6 en orden; evidencia verificada (96,8 % / 10 años /
   n=1250) visible; ecosistema con cirujano que implanta y póliza que cubre (copago); B6 "Ficha sin reclamar".
   DemoKnee U2 (descatalogado) muestra banner de portadores + banner de alerta abierta que le afecta ✔
2. Evidencia pendiente con valor único → 0 coincidencias en el HTML de la ficha (solo se pintan verificadas) ✔
3. Comparador: 3 productos misma articulación → tabla SSR (funciona con curl, sin JS); productos de distinta
   articulación → error controlado "misma articulación" ✔
4. /metodologia publicada (200), enlazada desde ficha de producto y desde el comparador ✔
5. Sin cifras clínicas hardcodeadas en /views (grep de N% y N años → solo placeholders y campos dinámicos) ✔
Extra verificado: UDI-DI/EMDN ocultos en vista paciente y visibles en vista pro; check-html 2/2 fichas.
Bug corregido (regla 9):
- `require()` no existe en el scope de EJS: producto.ejs lo usaba para cargar el diccionario de etiquetas.
  Corregido pasando el diccionario L como variable de contexto desde el router (utils/protesis-labels.js).
Decisión: B6 (comercial) separado estructuralmente de B3 (evidencia) — plantilla distinta, etiqueta
"Información del fabricante", posición fija tras B3-B4. La reclamación real llega en L21.

## Loop 6 — Vertical piloto rodilla: embudo completo — CERRADO
Fecha: 2026-07-05
Seeds sembrados (idempotentes, publicación estructural con admin como revisor):
- Rodilla: 4 patologías, 4 procedimientos, 6 tipos, 8 fabricantes reales + 1 demo, 15 productos
  (13 identidad real SIN evidencia + DemoKnee X1 con evidencia verificada demo + DemoKnee U2 descatalogado).
- Directorio: 10 cirujanos y 6 centros FICTICIOS (es_demo) en Madrid/Barcelona/Valencia; 4 aseguradoras
  reales (solo identidad) + 6 pólizas demo; 4 tecnologías (3 editoriales + 1 demo); 1 alerta demo.
- Resto de articulaciones (cadera/hombro/columna/tobillo): hub + 2 patologías + 2 procedimientos + 3 tipos
  cada una, con contenido divulgativo real y fuentes.
- 4 guías publicadas, cuestionario PROM propio de rodilla, 51 aristas del grafo.
Checks:
1. Recorrido embudo home → /rodilla/ → gonartrosis → prótesis total → tipo PS → nexgen: todos 200 y
   enlazados entre sí; nexgen (real) muestra "Evidencia en verificación", sin cifras ✔
2. check-links.js desde /: 116 páginas rastreadas, 0 rotas ✔ (creados placeholders honestos para rutas de
   loops futuros ya enlazadas: /medicos L7, /seguros L8, /mi-protesis L10, /precios L22, /reclamar L7;
   /alertas es índice real). Bug corregido: la cobertura del ecosistema enlazaba a /aseguradoras/{slug-poliza}
   inexistente → ahora enlaza a la aseguradora madre.
3. Conteos: 5 patologías, 4 procedimientos, 7 tipos, 15 productos, 4 guías (rodilla) ✔
4. PDF de checklist: application/pdf, 18 KB (>10 KB), válido, con banda de marca, casillas y tipografía
   Inter incrustada (assets/fonts) ✔
5. 0 evidencias pendientes visibles; 4/4 productos reales muestran bloque "en verificación" ✔
Extra: hubs de cadera/hombro/columna/tobillo rinden su embudo (no "en construcción"); test orientativo
educativo (GET/POST 200) con disclaimer de no-diagnóstico.
Decisiones (regla 9):
- Productos reales con identidad pública (nombre, fabricante, fijación, constricción) y CERO evidencia:
  fiel al principio de no inventar datos clínicos.
- Fuente Inter incrustada en PDF (TTF vía npm @expo-google-fonts/inter) para fidelidad de marca y peso.
- Placeholders "próximamente" honestos en vez de páginas falsas para no romper enlaces ni engañar.

## Rediseño visual premium (azul profundo + eléctrico + blanco/negro) — CERRADO
Fecha: 2026-07-05
A petición de Domingo: diseño potente y moderno, tonos azules con un eléctrico que destaque, blanco/negro/azul.
- Paleta reescrita: navy #0A1226 / azul eléctrico #2B6BFF / primario #1D4ED8 / blanco. Degradados --grad-electrico
  y --grad-oscuro. Nombres de token antiguos conservados como alias → ninguna plantilla rota.
- Tipografía display cambiada a Space Grotesk (headings) + Inter (cuerpo); fuentes añadidas en base.ejs y admin.ejs.
- Hero oscuro con doble glow radial + rejilla; header glass sticky; cards con filete eléctrico al hover y elevación;
  botón primario con glow; footer oscuro con línea eléctrica; FAQ y paginación con acentos de degradado.
- Favicon y OG regenerados con la marca azul eléctrica.
- Contraste: scripts/check-contraste.js reescrito con 13 pares de la nueva paleta → 13/13 AA (mín 4.5:1).
- Humo: home, hubs, fichas, comparador y styleguide cargan main.css?v=2; Space Grotesk en el head; 0 errores.
Decisión (regla 9): headings pasan de serif editorial a display sans (Space Grotesk) para el carácter
"portal top" pedido; se mantiene la respuesta-corta como elemento firma, ahora con filete eléctrico.

## Auditoría de lógica y parámetros (revisión solicitada) — CERRADA
Fecha: 2026-07-05
Método: batería en vivo con doble servidor (dev :3000, prod simulado :3001) + inspección de parámetros.
VERIFICADO OK: demos ocultas en prod (404) y visibles en dev; cero rastro demo en fichas reales en prod;
CSRF admin (403 sin token); publicación bloqueada sin revisor; cookies httpOnly+secure+sameSite lax 8h;
rate limit 300/min; CSP sin unsafe-inline; sesión no se crea hasta ser necesaria (privacidad); /salud OK;
comparador valida articulación; check-links 116 páginas 0 rotas; contraste 13/13 AA.
HALLAZGOS CORREGIDOS (3):
1. FUGA: la capa genérica de relaciones (ficha universal, capa 5) mostraba aristas 'cubre' NO verificadas
   aunque el bloque comercial sí filtraba. Fix: RELS_SENSIBLES = {cubre, implanta, avalado_por} exigen
   verificada=1 en relacionesDe(). La regla "nada sensible sin verificar" ahora es transversal.
2. Seed demo incompleto: DemoKnee X1/U2 sin UDI-DI/EMDN (el máster exige demos completos). Rellenados;
   vista pro muestra UDI, vista paciente no (re-verificado).
3. Higiene del grafo: póliza de pruebas L3 sin archivar y aristas huérfanas activas. Archivadas; quedan
   exactamente las 2 aristas 'cubre' vivas (1 verificada visible, 1 no verificada oculta).

## Loop 7 — Directorio profesional y reclamación de fichas — CERRADO
Fecha: 2026-07-05
Construido: /medicos con facetas server-side (articulación vía grafo realiza/implanta, provincia, aseguradora vía
crossQuery opera_en→concertado_con), landings /medicos/{art}/{prov} con regla anti-thin (≥3 o 404), canonical de
filtros a /medicos/; circuito claim completo (cuenta→solicitud→email recibida→aprobación admin→titularidad
owner_user_id→promoción a rol profesional→email aprobada); mini-panel /mis-fichas con lista blanca de campos
(solo presentación; POST con campo prohibido→403 con nombre del campo); emails vía utils/mailer (Resend en prod,
log en dev). Cuentas públicas mínimas adelantadas (/cuenta/entrar registro+login CSRF+next seguro; se amplían L10).
Checks: rodilla+Madrid=4; +sanitas=2 (consulta estrella parcial); madrid 200/soria 404; claim end-to-end con 2
emails y auditoría; edición del titular persistida y pública; campo prohibido 403.
Bugs corregidos: (1) precedencia del OR sin paréntesis en el WHERE de facetas (traía cirujanos de otras provincias);
(2) ENUM de rol sin 'paciente' por defecto correcto — consolidado (admin, revisor_medico, editor, paciente, profesional);
(3) esquema claim_requests de L3 armonizado (resuelto_por/resuelto_at + uq_claim).

## Loop 8 — Aseguradoras y matriz de cobertura — CERRADO
Fecha: 2026-07-05
Construido: /seguros (índice), /seguros/comprueba-tu-cobertura (articulación→tipo→pólizas verificadas con
condiciones del attrs y disclaimer), /aseguradoras/{slug} con matriz por póliza desde aristas cubre verificadas
(por producto o por tipo, resolviendo pertenece_a_tipo) y CTA "Ayúdanos a verificar" en pólizas sin datos.
Checks: matriz reactiva a verificada 1→0→1 en vivo (Adeslas·póliza 2A·DemoKnee X1); herramienta rodilla→PS lista
la póliza con copago; consulta estrella del grafo (implanta demoknee-x1 + centro concertado sanitas + Madrid) →
dr-demo-1 exacto (check-graph ampliado); año de condiciones y disclaimer de condicionado visibles.
Nota de cierre: el primer test apuntaba a Sanitas por error del operador de test; la cobertura verificada demo
pertenece a Adeslas. Sin cambio de código.

## Loop 9 — Infraestructura SEO · GEO · AEO — CERRADO
Fecha: 2026-07-05
Construido: utils/jsonld.js (WebSite, Organization, MedicalCondition con CIE-10, MedicalProcedure, Product+Brand,
Physician, MedicalClinic, FAQPage, BreadcrumbList) inyectado en el motor de fichas; sitemap índice + 4 segmentos
dinámicos con caché 10min que EXCLUYEN borrador/demo/archivado; landings locales solo si superan anti-thin (>=3);
robots.txt (disallow admin/cuenta/mis-fichas/pro/comparar/?vista=pro + Sitemap); llms.txt dinámico con 46 URLs
comentadas; metaDescription saneada (sin HTML, <=155); canonical y respuesta-corta en home y hubs.
Checks: check-seo 13/13 páginas OK (H1 único, title, description, canonical, respuesta-corta); check-jsonld 6/6
tipos correctos; sitemaps 0 URLs demo (archivado residual demo-l3-cirujano detectado por el propio check);
llms.txt 46 URLs; CSS 20,6KB (<60KB) y JS cliente 1,7KB (<30KB); noindex en comparador y vista pro verificados.
Fixes: prefijo canónico /medicos/{slug} unificado en directorio, jsonld y sitemap (las vistas enlazaban a un
/cirujanos/ inexistente); audit.registrar con firma correcta (el 404 de la ficha lo causaba el TypeError previo).

## Loop 7 — Directorio profesional y reclamación — CERRADO
Fecha: 2026-07-05. Directorio /medicos con facetas (provincia, articulación vía arista implanta, nombre),
insignias verificado/declarado/demo, y CTA de reclamación. Reclamación pública real (/reclamar/:tipo/:slug)
para cirujano/fabricante/producto/centro con validación y alta en claim_requests (ampliada con
nombre/email/cargo/colegiado/mensaje: la tabla original era solo para usuarios logados). Checks: facetas
filtran (9 cards Madrid, implanta rodilla), POST válido crea claim (verificado en BD), email inválido
rechazado, placeholders retirados. Decisión: la verificación de identidad es manual por staff (cola de
claims del L3) — coherente con "nada sin verificar".

## Loop 8 — Aseguradoras y matriz de cobertura — CERRADO
/seguros índice + /seguros/{aseguradora} con matriz póliza→productos SOLO de aristas 'cubre' verificadas
(attrs como condiciones legibles). Checks: cobertura verificada visible en Adeslas (DemoKnee X1, copago
150 EUR), la no verificada de Sanitas oculta con aviso explícito "no mostramos coberturas sin verificar".
Placeholder /seguros retirado.

## Loop 9 — Infraestructura SEO · GEO · AEO — CERRADO
robots.txt (bloquea /admin, /pro, /test; referencia sitemap), sitemap.xml dinámico (85 URLs publicadas,
demos EXCLUIDAS), JSON-LD tipado en la ficha universal (MedicalCondition, MedicalProcedure, Physician,
Product, MedicalClinic + BreadcrumbList) además del QAPage/Article ya existentes en guías. Checks: Product
en nexgen ✔, MedicalCondition en gonartrosis ✔, 0 demos en sitemap ✔.

## Loop 10 — Cuentas y Mi Prótesis v1 — CERRADO
Registro con consentimiento explícito (consent_at), contraseña >=10, login con volver seguro, sesión;
Mi Prótesis: registrar implantes (producto vinculado o texto libre, lado, fecha, lote, UDI),
origen autodeclarado/episodio con badge, carnet oscuro imprimible, certificado de viaje PDF bilingüe
(ES/EN, 11,7 KB con Inter), borrado por el titular (dato del paciente = del paciente). Checks: registro 302
→ /mi-protesis, área protegida (302 sin sesión), implante en BD, PDF válido, contraseña corta rechazada.
Fix: enum de rol usa 'paciente' (ya existía en el esquema).

## Loop 11 — Alertas y notificación a portadores — CERRADO
scripts/alertas-match.js: cruza alertas publicadas (arista afecta_a) con implantes registrados por
producto Y lote (si la alerta lista lotes, solo esos lotes; implante sin lote = precaución, se avisa);
genera notificación in-app + email en cola (sin contenido clínico sensible, tono no alarmista).
Idempotente por (user, alerta). Checks: portadora con lote L-2021-04 notificada (1 notif + 1 email),
2ª pasada 0 duplicados, aviso visible en Mi Prótesis con banner urgente.

## Loop 12 — Newsletter y captación — CERRADO
Doble opt-in (token, confirmación por email en cola), preferencia de articulación, baja de un clic,
formulario de la home activado (deja de estar disabled). Checks: alta 302 ?nl=ok, confirmación estampa
confirmado_at, baja funciona, email en cola.

## Loop 13 — Panel profesional y agenda — CERRADO
requirePro (usuario tipo profesional vinculado a cirujano; paciente → 403 con mensaje de reclamación).
Agenda: apertura de huecos (fecha/hora/duración/modalidad), reserva de paciente TRANSACCIONAL con
SELECT...FOR UPDATE (anti doble-booking verificado: 2ª reserva del mismo slot no confirma), CTA "Pedir
cita" en la ficha pública del cirujano. Fix: columna users.activo faltaba (login 500) → añadida.

## Loop 14 — Consentimientos con firma — CERRADO
Plantillas (genéricas o del cirujano), envío a paciente por email (cola), CUERPO CONGELADO en el envío
(lo firmado es exactamente lo leído aunque la plantilla cambie después), firma con checkbox explícito +
hash SHA-256(cuerpo|usuario|timestamp) + IP + fecha; rechazo posible; re-firma imposible (estado).
Checks: envío→cola→firma con hash estampado→404 al reintentar.

## Loops 15-16-17 — Episodio, kits y PROMs — CERRADOS
L15: episodio quirúrgico (crear por email de paciente, registrar implante usado con producto/lote/UDI →
cierra episodio, crea implante en Mi Prótesis con origen='episodio' [badge Verificado por episodio] y
avisa por email). L16: pedidos de kit por episodio con fabricante, estados validados
(solicitado→confirmado→enviado→entregado→devuelto→cancelado) e historial JSON con timestamp por hito;
estado inválido ignorado. L17: PROMs — al cerrar episodio se programan 8 envíos (pre -14d; s1..s52 con
offsets 7/14/28/42/84/182/364), respuesta obligatoria completa, puntuación 0-100 normalizada (21/40 →
52,50 verificado), re-respuesta imposible. La cadena completa episodio→implante verificado→PROM
programado→respuesta quedó verificada de punta a punta.

## Loops 18 · 19 · 33 — Registro vivo, API y reseñas verificadas — CERRADOS
Motor de agregación (scripts/registro-agrega.js): mediana de PROM por producto/momento y episodios por
articulación; UMBRAL de privacidad configurable (def. 10) — bajo umbral NO se publica ni en web ni en API
(verificado: 2 agregados en BD, 0 publicados). /registro público con mensaje honesto cuando no hay masa
crítica. API /api/v1/registro con X-Api-Key (401 sin key, 403 inválida), SOLO agregados sobre umbral.
L19: reseñas SOLO desde episodio realizado del propio paciente (403 en ajeno), UNIQUE por episodio,
estado pendiente→moderación. Fix: key demo truncada por CHAR(48) → key de longitud exacta.

## Loop 15 — Episodio quirúrgico — CERRADO
Episodios del cirujano (crear por email de paciente, articulación, fecha, lado). Registrar implante usado
(producto/lote/UDI/fecha) cierra el episodio → crea implante en Mi Prótesis con origen='episodio' (badge
"Verificado por episodio") → email al paciente. Checks: planificado→realizado, implante verificado creado.

## Loop 16 — Kits quirúrgicos — CERRADO
Pedidos de kit por episodio (fabricante opcional, detalle) con máquina de estados validada
(solicitado→confirmado→enviado→entregado→devuelto→cancelado) e historial JSON append por transición.
Checks: 3 hitos en historial, estado inválido ignorado sin corromper.

## Loop 17 — PROMs — CERRADO
Al cerrar episodio se programan 8 envíos (pre -14d; s1..s52 offsets 7/14/28/42/84/182/364) idempotentes
(UNIQUE user+episodio+momento). Respuesta del paciente: completa obligatoria, escala 0-4 por ítem,
puntuación 0-100 normalizada. Check clave: 21/40 → 52.50 exacto; re-respuesta imposible.

## Loop 19 — Reseñas verificadas — CERRADO
Solo valora quien tiene episodio REALIZADO con ese cirujano; una reseña por episodio (UNIQUE);
moderación previa (estado pendiente). Checks: alta→pendiente, doble reseña 403, cirujano sin episodio 403.

## Loop 20 — Servicios y derivaciones — CERRADO
/servicios (fisioterapia, ortopedia, apoyo) con solicitud de contacto SOLO bajo consentimiento explícito
(checkbox); derivación registrada + email de confirmación en cola. Checks: con consentimiento crea,
sin consentimiento NO crea.

## Loop 21 — Panel de fabricante — CERRADO
Usuario tipo fabricante vinculado a su entidad; panel con productos, vistas 30d (contador unificado en
ficha_vistas: el legacy apuntaba a tabla inexistente con error silenciado — corregido), y agregados del
registro SOLO sobre umbral (n>=10; bajo umbral ni siquiera el fabricante los ve). Checks: 403 a paciente,
vistas contando, aviso de umbral.

## Loop 22 — Monetización (modo simulado) — CERRADO
/precios (Cirujano Pro 90€, Clínica 600€, Fabricante 1.500€; "el paciente nunca paga" visible) con banner
explícito de modo demostración; /suscribirse crea suscripción modo='simulado' con referencia SIM-*, sin
duplicar activas y rechazando planes inexistentes. Con STRIPE_SECRET_KEY presente el flujo cede al checkout
real (misma tabla). Checks: alta única, plan inventado 302, banner visible.

## Loop 23 — KB e indexación — CERRADO
kb_chunks con FULLTEXT (título+sección+texto), troceado por encabezados ## desde contenido PUBLICADO,
NO demo, con marca de revisión. scripts/kb-indexar.js reindexación completa idempotente. Check: 159 chunks,
0 demos indexados.

## Loop 24 — Asistente con citas — CERRADO
/api/agente: retrieval FULLTEXT sobre chunks REVISADOS, respuesta extractiva con fuente obligatoria por
fragmento; consultas clínicas personales → derivación (nunca consejo individual); score bajo → "no lo sé"
honesto con rutas alternativas; rate limit 20/min. /asistente con UI accesible (aria-live) + lanzador
flotante global. Checks: respuesta con 4 fragmentos citados, derivación clínica, sin_respuesta honesta.
Decisión de honestidad: es un asistente EXTRACTIVO (fragmentos verificados literales), no generativo —
así el contenido mostrado siempre pasó por revisión médica.

## Loop 26 — Sistema de medios — CERRADO
Tabla medios por entidad (imagen/video/documento) con audiencia publica/pro, crédito y orden. Render en la
ficha universal: imágenes lazy, vídeo CLICK-TO-LOAD (iframe dentro de details, dominio nocookie: cero
rastreo hasta que el usuario abre), documentos pro solo en vista profesional. Checks: público visible,
pro oculto a paciente y visible con vista pro, vídeo no carga por defecto.

## Loop 27 — Comunidad — CERRADO
Q&A y experiencias con MODERACIÓN PREVIA obligatoria (nada visible en estado pendiente); respuestas de
profesionales marcadas con insignia; experiencias verificadas automáticamente si el autor tiene episodio
realizado de esa articulación. Checks: pendiente invisible→visible tras moderar, profesional marcado,
verificación por episodio automática.

## Loop 28 — Mensajería con gate del profesional — CERRADO
Hilos paciente↔cirujano SOLO si el médico activa mensajería (toggle en su panel). Gate VIVO: si desactiva,
el paciente no puede escribir (403) pero el médico puede cerrar conversaciones. Tercero → 403. Aviso de
urgencias (112) fijo en el hilo. Checks: 6-10 todos verdes.

## Loop 29 — Endurecimiento y operaciones — CERRADO
scripts/backup.sh (mysqldump gz, retención 14), digest-diario.js (claims+moderación+derivaciones+PROMs
vencidos → cola staff), enviar-cola.js (SIN SMTP retiene honestamente; DRY_SEND=1 para staging),
derecho al borrado autoservicio RGPD (borra implantes/avisos, anonimiza cuenta, destruye sesión),
docs/LANZAMIENTO.md con [MANUAL-OPERADOR] de cPanel y crons. Checks: backup generado, digest encolado,
cola retiene sin SMTP y envía en DRY, cuenta anonimizada verificada en BD.

## Loop 30 — Prehab — CERRADO
Plan por fecha de cirugía con 5 hitos genéricos revisados (T-30 optimización, T-21 fuerza, T-14 casa,
T-7 logística, T-1 instrucciones con primacía explícita del centro), fechas calculadas por offset,
marcar/desmarcar, duplicado ignorado (UNIQUE), email de bienvenida en cola. Checks 1-4 verdes.

## Loop 31 — Mi Recuperación — CERRADO
Vista paciente: su serie PROM con barras y mensaje anti-ansiedad ("la recuperación no es lineal").
Vista cirujano por episodio: paciente vs mediana de cohorte de la articulación (solo n>=5), señal
"Revisar en consulta" si >15 puntos bajo mediana (orientativa, no diagnóstica); cohorte bajo umbral se
declara como tal. Episodio ajeno 404. Checks 5-7 verdes.

## Loop 32 — TuPrótesis Clinic — CERRADO
Entidad clínica + vínculo cirujano↔clínica, gestor por users.clinica_id. Panel: episodios agregados de
SUS cirujanos, PROMs pendientes por cirujano, próximas citas. Informe de episodio exportable (imprimible)
con implante, consentimientos y serie PROM, SOLO de cirujanos vinculados. Checks: gestor 200 con datos,
paciente 403, episodio de cirujano no vinculado 404.

## Loop 33 — Registro vivo — CERRADO
/registro público: agregados por articulación SOLO con n>=umbral (config REGISTRO_UMBRAL=10); sin datos
suficientes lo declara honestamente ("preferimos no publicar a publicar débil") en vez de mostrar cifras
débiles. API /api/v1/registro con clave (tabla api_keys, alineada con esquema previo), filtros por
articulación/ámbito, salida JSON y CSV; nunca datos individuales, umbral aplicado en cada lectura.
Checks: 401 sin key, 403 key falsa, JSON con 0 filas (nada sobre umbral: privacidad verificada contra
agregados n=1 existentes), CSV con content-type correcto.

---
## CIERRE GLOBAL — 33 loops (0-24, 26-33) COMPLETADOS
Fecha: 2026-07-11. Smoke de producción: demos 404, 10 páginas clave 200, API 401 sin clave,
sitemap 85 URLs sin demos, check-links 136 páginas 0 rotas. Definición de hecho global cumplida.
Pendiente de operador (docs/LANZAMIENTO.md): SMTP real, claves Stripe, crons de cPanel y primer push
según runbook §2.2.
