114 historias en formato XP ("Como <actor>, quiero <acción> para <beneficio>"), consolidadas desde el kanban del proyecto (108 aceptadas de 114). Agrupadas por el rol del actor; cada una conserva su Definition of Done y la iteración de origen -ver en el tablero →.
Como visitante (21)
Como visitante, quiero ver el portfolio público con proyectos reales para evaluar el trabajo de Mike
- ✓BaseLayout, Navbar, Footer y la página principal con hero, proyectos y proceso quedan operativos.
- ✓ProjectCard consume datos reales de GitHub vía API de repos.
- ✓Tailwind CSS v4 configurado con paleta y tipografía del sitio.
Como visitante, quiero ver el estado en vivo de los servicios (uptime, incidentes, latencia)
- ✓/status muestra uptime global de 30 días e incidentes activos.
- ✓Animación EKG con latencia p95 en tiempo real por monitor (13 commits de status).
- ✓API de latencia expone último estado y hora de chequeo por monitor.
Como visitante, quiero que el sitio sea indexable y compartible (SEO técnico completo)
- ✓JSON-LD y breadcrumbs estructurados en páginas de proyecto.
- ✓Feed RSS y notificación IndexNow a buscadores en cada publicación (8 commits de IndexNow).
- ✓Web app manifest y apple-touch-icon para instalación como PWA.
Como visitante, quiero una vitrina pública de herramientas y notas técnicas del stack
- ✓Secciones /tools y /notes publicadas con 5 artículos técnicos iniciales.
- ✓OG images generadas dinámicamente para cada nota/proyecto.
Como visitante, quiero escanear un QR y ver cómo un tablero reconoce mi dispositivo en vivo sin cookies
- ✓Landing con consentimiento crea sala + QR; el tablero (/board) refleja los dispositivos por polling corto (Vercel no soporta WebSocket).
- ✓Al reentrar en incógnito o borrar cookies, el contador de "revisitas" sube: mismo dispositivo re-identificado (validado por smoke test de API).
- ·Prueba end-to-end en navegador real del ciclo crear → escanear → revisita (canvas/WebGL/audio solo producen valores en un browser).
Como visitante, quiero entender qué me delata: recolector propio contrastado con una librería y una capa de comportamiento
- ✓Recolector propio: canvas, WebGL, AudioContext, fuentes, pantalla, zona horaria, CPU/memoria, idiomas, touch y UA.
- ✓Segunda opinión con FingerprintJS (open source): su visitorId se muestra junto al hash propio en el tablero.
- ✓Entropía dinámica: cada señal bloqueada/vacía suma 0 bits; se declara como estimación educativa (EFF Panopticlick / AmIUnique), no medición poblacional.
- ✓Capa de comportamiento: cadencia de tecleo, velocidad de mouse y giroscopio (con permiso iOS 13+).
Como visitante, quiero explorar el portal de clientes de la demo con datos frescos, no con lo que dejó otro visitante
- ✓seedPortalDemo siembra clientes, facturas y client_users ficticios en la base demo, reusando scripts/seed-demo.mjs.
- ✓Cron de Vercel resiembra el portal demo a las 4am todos los días (vercel.json).
- ✓Endpoint público de demo añadido a las rutas accesibles sin sesión; pagos bloqueados también en el portal demo (middleware).
Como visitante, quiero que se me avise claramente cuándo la demo del portal está disponible
- ✓Banner de demo en PortalLayout y mensajes de disponibilidad en login y /tools.
- ✓Flag demoAvailable/demoUnavailable propaga el estado real de la base demo a la UI.
- ✓Tests de creación y validación del token de sesión demo del portal.
Como visitante, quiero analizar cualquier dominio público (SEO, performance, accesibilidad, TLS) desde el sitio
- ✓Endpoint público de análisis de dominio con rate limiting propio.
- ✓SSRF protegido: resolución de hostname y rechazo de IPs privadas/reservadas antes de solicitar el sitio.
- ✓Fetch del HTML memoizado y reutilizado entre las pruebas de SEO, performance y accesibilidad para no golpear el sitio objetivo varias veces.
- ✓Integración con Google PageSpeed Insights (Lighthouse) para las métricas de performance.
Como visitante, quiero entender qué hace la herramienta y confiar en que no expone mi sitio a riesgos
- ✓Sección "site analyzer" en /lab con enlace desde /tools y modal "How it works" explicando el proceso.
- ✓Nota técnica documentando cómo se analiza cualquier dominio sin poner en riesgo el propio servidor.
- ✓Normalización de emisor/sujeto del certificado TLS para mostrarlo de forma legible.
Como visitante, quiero una interfaz con más profundidad visual sin perder legibilidad
- ✓Blobs de fondo degradados añadidos a BaseLayout con posicionamiento propio, sin interferir con el contenido.
- ✓Scrim detrás del navbar para mantener contraste sobre contenido con scroll.
Como visitante, quiero descargar el CV desde la página de contacto
- ✓Enlace de descarga de CV añadido a la página de contacto.
- ✓/api/cv/capture recibe las señales del dispositivo y emite un token de un solo uso; /api/cv/download lo valida antes de servir el archivo.
Como visitante, quiero seguir rutas de aprendizaje con labs cronometrados y marcar mi progreso
- ✓educationPaths define rutas, temas y labs con duración y nivel (Inicial/Intermedio/Avanzado).
- ✓Tabla educationLabProgress persiste qué labs completó cada visitante.
- ✓API de progreso y toggle en /admin para gestionar el estado de cada lab.
Como visitante, quiero un fondo ambiental que se sienta vivo al hacer scroll sin distraer del contenido
- ✓AmbientBlobs.astro reemplaza los gradientes estáticos de global.css; cada blob se mueve de forma independiente con paralaje por scroll.
- ✓El desplazamiento combina ondas senoidales de frecuencia irracional por blob para evitar un patrón mecánico perceptible.
- ✓prefers-reduced-motion desactiva transform y transición por completo.
- ✓position/isolation en el body corrigen el stacking context tras integrar el componente en BaseLayout.
Como visitante, quiero encontrar rápido un enlace del footer sin escanear una sola lista larga
- ✓Footer.astro reagrupa los 12 enlaces en 5 categorías (Sitio, Producto, Ingeniería, Operación, Redes, Contacto directo).
- ✓Layout con flex-wrap reemplaza el grid de columnas fijas, evitando huecos vacíos en pantallas angostas.
Como visitante, quiero ver si el pipeline acaba de funcionar, no solo un diagrama de cómo funcionaría
- ✓/docs/pipeline-en-vivo muestra el estado real de la última corrida, etapa por etapa, con recorrido paso a paso.
- ✓src/lib/lab/pipeline-live.ts aísla la normalización y el mapeo de estados como lógica pura; tests/pipeline-live.test.ts la cubre.
- ✓La etapa de push refleja la ocurrencia del evento, no el resultado de la corrida.
Como visitante, quiero que el sitio se sienta fluido al desplazarme sin que el scroll se vuelva pesado
- ✓Lenis para el scroll suave y GSAP para las animaciones de entrada, integrados en el layout público.
- ✓autoRaise retirado de la inicialización de Lenis por su coste en rendimiento del scroll.
Como visitante, quiero que ningún enlace en inglés me lleve a una página que no existe
- ✓TRANSLATED_ROUTES como única fuente de verdad de qué existe en inglés; localizedHref cae al español si no hay traducción.
- ✓El hreflang y el sitemap solo anuncian idiomas en los que la página existe de verdad.
- ✓El middleware redirige GET/HEAD de /en sin traducir a la versión en español con 302, no 308.
- ✓tests/i18n-routing.test.ts cruza la lista contra los archivos reales de src/pages/en/ en las dos direcciones.
Como visitante, quiero recorrer la demo del panel con datos que no parezcan abandonados
- ✓Variables de la base demo en Production y Preview; base re-sembrada con 90 días de historial que terminan hoy.
- ✓resetSchema usa executeMultiple: contra Turso por HTTP cada execute viaja en su propia sesión y el pragma de foreign_keys se perdía.
- ✓Verificado en los dos backends (Turso y archivo local), dos corridas seguidas.
Como visitante, quiero entender qué se contrata antes de escribir
- ✓Landing /capacitacion-ia con los programas publicados desde el panel: formato, nivel, duración, objetivos, temario desplegable y nota de precio. Ningún dato del catálogo se escribe en el .astro.
- ✓Si la lectura del catálogo falla, la landing sigue sirviendo hero, proceso y contacto en vez de caer.
- ✓El material del grupo va con noindex y Cache-Control private, no-store: la respuesta depende de una cookie y una copia compartida se le serviría a quien no tiene pase.
- ✓capacitacion y capacitacion-ia registradas en RESERVED_ROOT_SEGMENTS: son rutas de un segmento en la raíz y competían con el espacio de los PIN de presentación. Lo cazó el test del repo, no una revisión.
Como visitante, quiero que el sitio siga en pie cuando la base no responde
- ✓safeQuery: una consulta que falla en una página pública devuelve su valor de reemplazo y el dato se pinta como ausente, nunca como error y mucho menos como 404.
- ✓Aplicado a las 23 consultas de las cinco páginas públicas con datos (portada, /status, /security, /certifications, /engineering); no queda ninguna consulta desnuda.
- ✓Cada fallo se registra con etiqueta en el log del servidor: degradar para el visitante no puede significar degradar en silencio para quien opera.
- ✓Excluido a propósito del panel, el portal y todo lo que cobra o autentica, donde un dato ausente en silencio es peor que un error visible.
Como administrador (30)
Como administrador, quiero autenticarme y que solo yo pueda entrar a /admin
- ✓Middleware de Astro protege todas las rutas /admin y /api/admin.
- ✓Auth.js con proveedor GitHub y allowlist de logins permitidos.
- ✓Base de datos Drizzle + libSQL con el esquema inicial de clients/projects/messages/finances.
Como administrador, quiero un dashboard con KPIs y gestión de proyectos, clientes y finanzas
- ✓Dashboard admin con KPIs de proyectos, mensajes, clientes y finanzas.
- ✓Sidebar de navegación agrupada y FinanceTable/MessagesList funcionales.
- ✓Formulario de contacto público con validación y persistencia de mensajes.
Como administrador, quiero backups automáticos de la base de datos para no perder información
- ✓API de backups sube snapshots a Vercel Blob mediante cron programado.
- ✓Página /admin/backup lista y permite crear backups manualmente.
Como administrador, quiero iniciar sesión con GitHub en vez de usuario/contraseña
- ✓Proveedor Credentials reemplazado por GitHub Provider en Auth.js.
Como administrador, quiero un dashboard, sidebar y páginas de certificaciones con mejor UX
- ✓Dashboard, Sidebar, AdminLayout y contacto rediseñados con mejor jerarquía visual.
- ✓CertCard y la página de certificaciones muestran estado (vigente/expirada) correctamente.
Como administrador, quiero monitorear la salud de mis servicios en producción
- ✓Esquema monitors/monitor_checks/monitor_incidents con checks HTTP periódicos.
- ✓Página /admin/monitors con estado, latencia e incidentes por monitor.
- ✓13 commits dedicados a monitoreo durante la iteración.
Como administrador, quiero que las variables de entorno de mis proyectos estén cifradas
- ✓Utilidades de cifrado/descifrado (crypto.ts) para project_env_vars.
- ✓Revelado de secretos on-demand vía fetch, sin exponerlos en el HTML inicial.
Como administrador, quiero gestionar dominios y ver su estado DNS/SSL
- ✓Página /admin/domains con verificación de dominios (8 commits del periodo).
Como administrador, quiero un pipeline de seguimiento comercial (leads → propuesta → cierre) y briefings de cliente
- ✓Página /admin/seguimiento con tablero de interacciones por proyecto.
- ✓Módulo de briefings con formulario e ítems asociados.
Como administrador, quiero presentar slides a clientes con control remoto
- ✓presentations/presentation_slides con reveal.js.
- ✓Vistas /admin/slides/[id]/present y /control sincronizadas (8 commits).
Como administrador, quiero una demo read-only del panel admin para mostrar a reclutadores
- ✓Entregada en Fase 10 - ver PF-LD-02.
Como administrador, quiero un sensor que observe y clasifique requests hostiles sin bloquear tráfico legítimo
- ✓classify.ts clasifica requests por firmas conocidas de ataque.
- ✓sensor.ts observa cada request de forma síncrona y no bloqueante (fire-and-forget).
- ✓security_events y security_rollups agregan eventos para el panel.
Como administrador, quiero bloquear IPs maliciosas y aplicar rate limiting que sobreviva a un redeploy
- ✓blocklist.ts responde 403 seco a IPs bloqueadas, con lectura cacheada 30s.
- ✓rate_limit_buckets: ratelimit-durable.ts reemplaza el rate limiting en memoria (9 commits).
- ✓Todo el bloque de enforcement es fail-open: un fallo nunca tumba el sitio.
- ✓Tests de blocklist y clasificación de seguridad en tests/.
Como administrador, quiero anomalías de seguridad agregadas en un panel para revisión periódica
- ✓security_anomalies almacena desviaciones detectadas sobre los rollups.
- ·Panel /admin/security con vista consolidada de anomalías y acciones de respuesta.
Como administrador, quiero registrar y gestionar mis llaves de seguridad desde el panel
- ✓Tabla webauthnCredentials (credentialID, publicKey, counter, transports, nickname) con índice por login.
- ✓/admin/passkeys lista, añade (con nickname) y revoca llaves; excludeCredentials evita re-registrar la misma llave física.
- ✓API /api/admin/webauthn/{registration,credentials} protegida por el gate de sesión+allowlist existente.
Como administrador, quiero entrar a /admin tocando mi llave, sin pasar por GitHub
- ✓Flujo usernameless (sin allowCredentials): el navegador ofrece las llaves discoverable del rpID y el login se descubre por el credentialID devuelto.
- ✓Proof HMAC de 30s (signPasskeyProof/verifyPasskeyProof) conecta la ceremonia FIDO2 con el provider "passkey" de Auth.js, sin repetir la criptografía ahí.
- ✓rpID/origin derivados del Host de cada request (prod, previews de Vercel y dev en cualquier puerto), no hardcodeados.
Como administrador, quiero que el selector de llaves del navegador distinga cada YubiKey por su nombre
- ·Detectado en uso real (2026-07-12): con 2+ llaves registradas, el diálogo nativo "Choose a passkey" de Chrome muestra "mikerb95 / USB security key" repetido para cada una, en vez del nickname (YubiKey 1, YubiKey 2) - porque userDisplayName se fija en generateRegistrationOptions con el login, no con un nombre por llave, y queda grabado en el hardware al registrar.
- ·Fix: pasar un userDisplayName distinto por llave (ej. incluir el nickname) en buildRegistrationOptions, y re-registrar las llaves existentes para que tomen el nuevo nombre - el cambio no aplica retroactivo a credenciales ya grabadas.
Como administrador, quiero una demo read-only del panel admin para mostrar a reclutadores sin exponer datos reales
- ✓/demo firma un token de sesión de solo lectura y lo valida en middleware sin tocar Auth.js.
- ✓Middleware bloquea cualquier método no-GET en modo demo y marca el flag demo en locals.
- ✓scripts/seed-demo.mjs siembra clientes, proyectos, finanzas, monitores y CI runs ficticios en una base aislada.
- ✓Tests de verificación de token demo y restricción de métodos.
Como administrador, quiero que la presentación privada del deck no sea accesible desde la demo pública
- ✓Middleware distingue isPrivate de isAdmin para aplicar headers de seguridad correctos.
- ✓Acceso al deck de presentación restringido a sesión de administrador real, excluyendo la sesión demo.
Como administrador, quiero cobrarle a un cliente por WhatsApp con un enlace de pago corto, sin que necesite cuenta
- ✓Página /cobrar genera un cobro con código corto (alfabeto/crypto propios) y arma el mensaje de WhatsApp.
- ✓/pagar simula el pago del cobro validando titularidad de la factura, reutilizando la máquina de estados de payments.
- ✓Webhook de conciliación liquida o reversa el pago y actualiza la factura asociada.
- ✓Cron diario (sweep) marca facturas vencidas y notifica al cliente automáticamente.
- ✓Tests de cálculo de facturas, formato de dinero en COP y expiración de cobros.
Como administrador, quiero activar clientes en el portal, invitar usuarios y gestionar sus facturas e hitos desde /admin
- ✓/admin/portal activa clientes, invita usuarios y lista su estado (invited/active/disabled).
- ✓/admin/portal/facturas y /admin/portal/hitos con CRUD completo de invoices y milestones.
- ✓/admin/portal/mensajes lista y responde hilos de conversación por cliente, con notificación al responder.
- ✓Enlaces añadidos al sidebar de administración.
Como administrador, quiero que cada push escanee vulnerabilidades de dependencias y del propio código
- ✓.github/workflows/security.yml corre npm audit y CodeQL en cada push/PR.
- ✓scripts/npm-audit-scan.mjs normaliza el reporte de npm audit para ingesta.
- ✓security_findings agrega hallazgos por herramienta con severidad, estado y auto-resolución cuando el hallazgo desaparece.
Como administrador, quiero saber si las páginas públicas cumplen accesibilidad básica (axe-core)
- ✓scripts/a11y-scan.mjs recorre páginas públicas con Playwright + @axe-core/playwright.
- ✓.github/workflows/a11y.yml ejecuta el escaneo en CI y sube resultados al ingest.
- ✓Umbrales de contraste de color ajustados tras los primeros hallazgos reales.
Como administrador, quiero saber si mis tests realmente detectan bugs (mutation testing), no solo si pasan
- ✓stryker.config.json configura mutation testing sobre los módulos críticos, con reporte HTML y concurrencia ajustada.
- ✓.github/workflows corre Stryker y scripts/mutation-scan reporta el score al panel LAB (mutationScore en ci_runs).
- ✓Pipeline y /lab muestran el mutation score cuando hay un resultado reciente disponible.
Como administrador, quiero pruebas de contrato que fallen si un endpoint cambia su forma de respuesta sin querer
- ✓Esquemas zod (health, checkout, status/latencia, SLO) definen la forma esperada de cada respuesta.
- ✓tests/contracts.test.ts valida los endpoints clave contra esos esquemas.
- ✓Roadmap actualizado: etapas 1–6 marcadas como completadas.
Como administrador, quiero ver el portal exactamente como lo ve un cliente, para dar soporte sin pedirle su clave
- ✓Botón "Ver como cliente" en la lista de clientes con usuarios de portal activos.
- ✓API de impersonación crea una portal_session marcada con impersonatedBy, visible en PortalLayout con aviso y logout que restaura la sesión admin.
- ✓Modo impersonado restringido a solo lectura: bloquea pagos y cualquier acción de escritura; queda registrado en auditoría (impersonate.start/end).
Como administrador, quiero ver quién descargó mi CV y detectar revisitas del mismo dispositivo
- ✓cv-downloads.ts reutiliza el deviceHash del recolector de fingerprinting; sin TTL, a diferencia de las salas de la demo.
- ✓Revisitas del mismo deviceHash actualizan la fila existente (contador revisits) en vez de crear duplicados.
- ✓/admin/lab/cv-downloads y su API listan el historial completo con IP, UA y referer.
Como administrador, quiero que una IP reincidente reciba un bloqueo cada vez más largo, no siempre el mismo TTL
- ✓blockIpEscalated calcula TTL creciente (1h → 24h → 7d) según el historial de bloqueos previos de la IP.
- ✓Honeypots tocados se bloquean inline desde el middleware, sin esperar al cron de auto-block.
- ✓Tests unitarios de blockIpEscalated y de la lógica de TTL en isBlocked.
Como administrador, quiero que la aplicación corriendo también se ataque de forma automatizada, no solo se lea el código
- ✓Workflow dast.yml corre el baseline de OWASP ZAP contra el preview deployment, con guard para no tocar producción.
- ✓parseZapReport convierte el reporte JSON en hallazgos, reutilizando el fingerprint de deduplicación existente en vez de una tabla nueva.
- ✓Tests del parser sobre reportes reales (tests/findings.test.ts).
- ✓DAST incorporado como nivel propio en el inventario de pruebas y en la clasificación de V&V.
Como administrador, quiero presentarme por WhatsApp y dejar agendar una llamada sin cruzar correos
- ✓Constructor de mensajes de WhatsApp para presentaciones rápidas.
- ✓Enlace de agenda directa en /contact, junto al formulario.
Como stakeholder académico (4)
Como stakeholder académico, quiero documentación de requerimientos funcionales y user stories
- ✓Documento de requerimientos funcionales para gestión de proyectos y clientes.
- ✓User stories de visitantes públicos y funcionalidades de administrador.
Como stakeholder académico, quiero navegar la documentación del proyecto sin necesitar sesión de administrador
- ✓Páginas de casos de uso, diagramas, historias de usuario, requisitos y kanban migradas de /admin/docs a /docs.
- ✓DocsNav como componente de navegación propio, reemplazando AdminLayout en todas las vistas de documentación.
- ✓Redirect catch-all de /admin/docs a /docs para no romper enlaces existentes.
Como stakeholder académico, quiero ver cada requisito con su verificación, notas y requisitos relacionados
- ✓Interfaz Requisito ampliada con campos de verificación, notas técnicas e ítems relacionados.
- ✓Modal de detalle por ítem en RequisitosView con atributos de datos para accesibilidad.
- ✓Diagramas Mermaid (casos de uso, secuencia, clases, componentes) con tema propio y mejor contraste.
Como stakeholder académico, quiero un deck de presentación que resuma el proyecto para la sustentación
- ✓/docs/presentacion.astro consolida arquitectura, fases y decisiones clave del proyecto.
- ✓Enlace al deck desde DocsNav y desde la documentación principal.
Como visitante técnico (4)
Como visitante técnico, quiero un panel público con métricas de ingeniería reales (Web Vitals, CI, disponibilidad)
- ✓/engineering muestra Core Web Vitals p75 de usuarios reales (RUM) con p50/p95, distribución de rating, tendencia 7d y peor ruta por métrica.
- ✓Cards de pipeline CI (tests, cobertura, duración) y disponibilidad agregada, alimentadas de ci_runs y monitor_checks.
- ✓CardPopover en cada métrica explica el origen del dato y el umbral contra el que se evalúa; página server-rendered con datos de producción.
Como visitante técnico, quiero comprobar que las métricas de /engineering están vivas y no hardcodeadas
- ✓Endpoint /api/engineering/live devuelve marcas de tiempo frescas (última muestra RUM, último sondeo, último run CI) más el reloj del servidor.
- ✓Las cards consultan la prueba de vida al abrirse e indican en vivo la frescura; solo expone metadatos, nunca URLs internas ni configuración.
- ·Merge del PR #2 a main y despliegue a producción de la verificación en vivo.
Como visitante técnico, quiero ver un laboratorio con los experimentos de CI/CD y su resultado más reciente
- ✓/lab lista experimentos con conteo de runs y fecha del último, alimentado por ci_runs.
- ✓Enlace "Laboratorio" en footer y navbar, y desde el caso de estudio de tools.
- ✓Ruta /lab añadida a STATIC_PATHS del sitemap.
Como visitante técnico, quiero ver en /lab si hay hallazgos abiertos de seguridad o accesibilidad
- ✓/admin/lab/security lista hallazgos con filtro por estado y acción de marcar resuelto (API GET/PATCH).
- ✓/lab muestra card de seguridad y accesibilidad condicionada a que exista ingesta reciente, con conteo agregado por estado.
- ✓Tests de procesamiento de hallazgos y transición de estados.
Como responsable del sitio (1)
Como responsable del sitio, quiero que la demo sea ética y segura: efímera, consentida y con defensas anti-abuso
- ✓Salas efímeras: TTL de 2h purgadas por el cron (sweepFpRooms), sin PII persistente; consentimiento explícito antes de recolectar.
- ✓Rate limiting durable por endpoint (beat reescopado por dispositivo para eventos con muchos móviles tras una NAT).
- ✓entropyBits acotado a 0–64 en servidor y valores escapados en el DOM (el UA es controlable).
- ✓Cierre pedagógico: por qué el incógnito no protege y cómo defenderse (Tor, resistFingerprinting).
Como cliente (7)
Como cliente, quiero iniciar sesión en mi propio portal con invitación previa y recuperar mi contraseña si la olvido
- ✓client_users, client_invitations y portal_sessions con hashing scrypt y sesiones opacas revocables (cookie portal_session, distinta de admin).
- ✓APIs de login, logout, aceptar invitación y reset de contraseña, con rate limiting y logging al micro-SIEM.
- ✓PortalAuthLayout para las pantallas de login, invitación y reset; requirePortalSession() como única puerta de entrada al tenant.
- ✓Tests de hashing de contraseña, clasificación de rutas del portal y aislamiento entre tenants.
Como cliente, quiero ver el estado de mis proyectos, salud del servicio y mis facturas en un panel propio
- ✓Página de overview del proyecto con HealthCard (salud de monitores) y MilestoneTimeline (hitos con estado).
- ✓Selector de proyecto (tabs) cuando el cliente tiene más de uno, resuelto por ?p= solo contra la lista de proyectos de su propia sesión - un id ajeno cae al primero propio.
- ✓Módulo invoices/invoice_items con CRUD completo, numeración correlativa y estados (draft/sent/paid/overdue/void).
- ✓Centro de notificaciones in-app e hilos de mensajería (portal_threads/portal_messages) con conteo de no leídos.
Como cliente, quiero gestionar mi cuenta, mi equipo, mis documentos y ver mi historial de pagos
- ✓/portal/cuenta con perfil, cambio de contraseña y gestión de sesiones activas.
- ✓Gestión de equipo (invitar/roles) reutilizando client_invitations desde el propio cliente.
- ✓/portal/documentos sube y descarga archivos vía stream autenticado (se descartó URL firmada por seguridad) con auditoría.
- ✓/mis-pagos resuelve el historial por teléfono con enlace firmado y rate limiting.
Como cliente, quiero pagar un cobro real por WhatsApp y recibir una notificación cuando se apruebe
- ✓Checkout API en /api/c/[code]/checkout.ts integra Wompi para el cobro real, además del mock de pruebas.
- ✓notifyCobroPaid envía alerta push al admin cuando un cobro se marca pagado.
- ✓Rate limiting propio para las rutas de enlace de cobro (isCobroLinkPath).
Como cliente, quiero descargar mi factura en PDF para mi contabilidad
- ✓/api/portal/facturas/[id]/pdf genera el PDF de la factura al vuelo con pdf-lib.
- ✓Enlace de descarga de PDF añadido a cada factura en el portal del cliente.
Como cliente, quiero ver la respuesta a mi mensaje sin recargar la página
- ✓portalLiveDigest() sobre los helpers existentes: cero SQL nuevo.
- ✓GET /api/portal/live con rate limit por sesión (10/min) y no-store.
- ✓Ciclo de 20 s con pausa por visibilidad y backoff 20→300 s; fail-open silencioso.
- ✓Un CustomEvent y tres suscriptores: campana, dashboard e hilo abierto.
- ✓tests/portal-live.test.ts con libSQL real: aislamiento entre clientes, rol billing sin mensajes, projectId ajeno cae al propio.
Como cliente, quiero una línea de tiempo de lo que ha pasado en mi proyecto
- ✓Migración aditiva de portal_activity con sus índices; clientId denormalizado para no depender de un JOIN.
- ✓recordActivity() fire-and-forget cableado en los cinco puntos que ya notificaban; nunca lanza.
- ✓Feed en /portal (últimas 15) y /portal/actividad con filtro y paginación por cursor.
- ✓Las entradas se apagan desde /admin/portal/actividad en vez de borrarse.
- ✓tests/portal-activity.test.ts: aislamiento, cursor que no repite ni salta filas, interruptor de visibilidad.
Como visitante de la demo (1)
Como visitante de la demo, quiero explorar el portal de clientes sin poder pagar ni modificar nada real
- ✓Rutas de pago bloqueadas explícitamente en modo demo, incluso con sesión de solo lectura válida.
- ✓Enlace al portal/demo del panel P&L añadido al caso de estudio de tools y al login.
- ✓README documenta el módulo de portal y cobros junto a demo y lab.
Como equipo de QA (2)
Como equipo de QA, quiero un arnés de pruebas end-to-end contra bases de datos desechables
- ✓Playwright configurado con fixtures propios y script para crear/sembrar bases de datos desechables por corrida.
- ✓e2e/demo.spec.ts valida acceso y aislamiento de datos de la demo; suite de páginas públicas y headers de seguridad.
- ✓webServer de Playwright siembra la base antes de levantar el servidor de pruebas.
Como equipo de QA, quiero que las pruebas E2E corran automáticamente en cada push, no solo en mi máquina
- ✓Job de Playwright añadido a .github/workflows/ci.yml, corriendo contra base de datos desechable sembrada.
- ✓Roadmap actualizado marcando el E2E de Playwright como completado, con siguientes pasos definidos.
Como desarrollador (3)
Como desarrollador, quiero un único punto de acceso a variables de entorno que funcione igual en Astro, scripts y tests
- ✓serverEnv() en src/lib/env.ts unifica el acceso a import.meta.env/process.env; checkout, cobros y mis-pagos migrados.
- ✓.env.local y variantes locales (.bak incluido) excluidas de git para evitar fugas de credenciales de Wompi/Turso.
- ✓Tests E2E de checkout (con idempotencia) y de formulario de contacto (validación y rate limiting) con IPs únicas por caso.
Como desarrollador, quiero un único parser de tech stack para no repetir lógica de formato en cada vista de proyecto
- ✓parseTechStack maneja tanto JSON como texto plano de forma tolerante.
- ✓ProjectCard, index y [id].astro migrados al parser único, eliminando JSON.parse directo.
Como desarrollador, quiero que clonar el repositorio produzca el mismo entorno en cualquier máquina
- ✓.devcontainer/ con Node 22.12 exacto, Chromium de Playwright preinstalado y usuario no-root.
- ✓Imágenes pineadas por digest, no por tag: una reproducibilidad que depende de que nadie mueva latest no lo es.
- ✓compose.yaml levanta dos servidores sqld (principal y demo), como en producción, con npm run db:up.
- ✓Contenedores con cap_drop ALL y solo las capacidades medidas como imprescindibles.
- ✓Documentado que Docker no es ni debe ser el runtime de producción: contenerizar la app perdería edge, previews y rollback automático.
Como asistente a la charla (1)
Como asistente a la charla, quiero una landing con la agenda y la forma de contactar al ponente
- ✓/platziconf con tema visual propio (platzi-theme) y CTA a Calendly.
- ✓Fondo full-bleed consistente en pantallas anchas.
Como asistente (2)
Como asistente, quiero ver el estado de la presentación (slide actual) sin tener sesión de administrador
- ✓API de estado de presentación expuesta sin requerir sesión admin, consumida por la vista de presentación.
- ✓Botón para copiar el enlace de la slide actual al portapapeles desde /admin/slides.
Como asistente, quiero entrar al material de mi grupo sin crear otra cuenta
- ✓Código de grupo que se dicta al cerrar la sesión, con alfabeto sin caracteres ambiguos (sin O/0, I/1/l, S/5) y normalizado al comparar; se canjea por un pase firmado de 30 días, sin correo ni cuenta.
- ✓El id del código viaja dentro de lo firmado y se revalida contra la base en cada request: revocar corta el acceso de esa cohorte al instante, sin tocar el de las demás.
- ✓Único punto del repo que falla CERRADO: sin poder confirmar que el código sigue vivo se cae al banco público, porque abrir publicaría material restringido en una respuesta cacheable.
- ✓El canje devuelve el mismo mensaje para código inexistente, vencido, revocado o mal escrito, y lleva rate limit propio (10/min por IP) sobre el mismo enforcement durable de los links de cobro.
- ✓tests/capacitacion.test.ts: 35 casos. Firma con otro secreto, sin secreto, vencimiento reescrito, id de cohorte reescrito y tokens malformados, todos rechazados sin lanzar.
Como visitante que revisita la sala de fingerprinting (1)
Como visitante que revisita la sala de fingerprinting, quiero que el tablero me reconozca aunque el hash de la librería de referencia cambie
- ✓joinDevice usa el ID único de dispositivo (no la etiqueta) para decidir si es una revisita, y ya no depende de que libFpHash coincida exactamente.
- ✓Tests con distintos escenarios de libFpHash (coincide, cambia, ausente) validando que la revisita se reconoce igual.
Como compañero de clase (1)
Como compañero de clase, quiero un único punto de entrada que me explique cómo se prueba este sistema
- ✓/docs/testing cubre los niveles de prueba, la pirámide con volúmenes reales, el pipeline etapa por etapa y un glosario.
- ✓Las métricas salen de src/data/testing.ts y de la ingesta del pipeline, no se escriben a mano en la página.
- ✓Restricción OPSEC respetada: los tests de seguridad se describen por lo que garantizan, sin citar patrones, rutas señuelo ni umbrales.
Como evaluador (3)
Como evaluador, quiero ver el testing clasificado bajo el marco de verificación y validación
- ✓/docs/verificacion-validacion reclasifica los niveles bajo V&V con niveles de integridad y procesos del ciclo de vida.
- ✓src/data/vyv.ts referencia los niveles de testing.ts por id en vez de duplicarlos, y un test verifica esa integridad referencial.
Como evaluador, quiero el diagrama de paquetes que faltaba en la documentación UML
- ✓/docs/diagrama-paquetes con la representación UML de los paquetes del sistema y su versión ilustrada.
- ✓Pestaña «Paquetes» añadida a DocsNav.
Como evaluador, quiero ver la vista de red y las interacciones en notación UML de verdad
- ✓Diagrama de despliegue con nodos «device»/«executionEnvironment», artefactos y caminos de comunicación con su protocolo.
- ✓Cuatro diagramas de comunicación con numeración decimal, enlazados en ambos sentidos con los de secuencia.
- ✓Tres diagramas de actividades con particiones, bifurcación concurrente y bucle de reintento.
- ✓SVG generado en el servidor desde modelos tipados; el navegador no ejecuta JavaScript para dibujarlos.
Como dueño de un negocio local (1)
Como dueño de un negocio local, quiero entender qué me ofrecen y cuánto cuesta sin lenguaje técnico
- ✓/paginas-web con tres planes de precio visible, perfiles de cliente, pasos del proceso y FAQ.
- ✓Contacto por WhatsApp o formulario, reutilizando /api/contact con su rate limiting ya probado.
- ✓Enlace «Diseño Web» en el navbar; la landing vive aparte de la marca técnica del resto del sitio.
Como cliente que navega con teclado o lector de pantalla (1)
Como cliente que navega con teclado o lector de pantalla, quiero saber qué pasó al enviar un formulario del portal
- ✓Skip link como primer elemento tabulable del portal, con el <main> identificado como destino.
- ✓Región aria-live que anuncia el resultado del envío, incluidos los mensajes de error explícitos.
- ✓El foco aterriza en la confirmación tras enviar, en vez de quedarse donde estaba.
Como jurado (3)
Como jurado, quiero ver los procesos de negocio modelados en BPMN, no descritos en prosa
- ✓/docs/diagrama-bpmn renderiza cuatro procesos (monitoreo, cobro de campo, acceso al portal, seguridad) desde datos tipados en src/data/bpmn.ts.
- ✓src/lib/bpmn-layout.ts calcula carriles, canales de transición y cajas de etiqueta como lógica pura; 83 tests en tests/bpmn.test.ts.
- ✓Detección de colisiones de etiquetas y de texto fuera de los límites dentro del propio motor de layout.
- ✓Eventos temporizadores de frontera con su tabla de tiempos por proceso.
Como jurado, quiero que las cifras de /docs/testing sean la corrida real, no un número escrito a mano
- ✓scripts/report-tests.mjs lee el XML JUnit de "npx vitest run --reporter=junit" y genera src/data/ejecucion-pruebas.json (versionado, porque coverage/ e informes/ están en .gitignore y Vercel no puede leer lo que no está en el repo) más un informe HTML autocontenido.
- ✓/docs/ejecucion-pruebas publica el total real de la última regeneración (1181 tests y 0 fallos en la entrega del 21 ago; 1200 en 70 archivos tras regenerarla el 26 ago), duración por nivel, distribución logarítmica de tiempos y las 12 pruebas más lentas.
- ✓METRICAS_REFERENCIA, NIVELES y PIRAMIDE en src/data/testing.ts pasan a derivarse de la instantánea (EJECUCION) en vez de tener sus propios números, que era justo la fuente de la contradicción (724 vs 777 tests en dos tablas distintas del mismo documento).
Como jurado, quiero ver qué zona puede alcanzar a cuál y con qué protocolo, no solo dónde corre cada nodo
- ✓src/data/red.ts modela zonas (con nivel de confianza), hosts y flujos dirigidos; src/lib/red-layout.ts calcula la geometría reutilizando el motor genérico del BPMN (corte de texto, polilíneas redondeadas).
- ✓NetworkDiagram.astro renderiza el SVG en el servidor; /docs/diagrama-red lo publica con la tabla de controles por cruce de frontera.
- ✓OPSEC respetado: solo entra lo ya público (puertos estándar, protocolos, proveedores y controles por categoría), nunca umbrales de rate limit ni nombres de reglas de detección.
Como sustentante (1)
Como sustentante, quiero entregar los diagramas en papel sin perder legibilidad
- ✓/docs/bpmn-imprimible con portada, un proceso por página y diagramas en vertical.
- ✓scripts/export-bpmn.mjs exporta cada proceso a SVG y PNG en docs/diagramas-bpmn/.
- ✓Documento de arquitectura (DEA) y taller de testing de caja negra actualizados junto a los diagramas.
Como visitante internacional (1)
Como visitante internacional, quiero leer el sitio en inglés sin perder la página en la que estoy
- ✓Prefijo /en con el español como idioma canónico sin prefijo; el selector de idioma conserva la página actual.
- ✓Siete páginas de marca traducidas vía diccionario: home, engineering, tools, security, contact, certifications y architecture.
- ✓Una sola implementación por página: la variante /en es un cascarón que reexporta la misma página y el locale sale de la URL del request.
- ✓hreflang recíproco con x-default, canónico localizado, og:locale y feed propio /en/rss.xml.
- ✓Sugerencia de idioma por Accept-Language como invitación descartable, nunca como mecanismo de ruteo (rompería el cache público).
Como responsable del sistema (1)
Como responsable del sistema, quiero que ningún guarda de seguridad cambie de veredicto por un prefijo de idioma
- ✓El middleware normaliza el pathname una sola vez, antes de cualquier clasificación de amenazas o de auth.
- ✓Las rutas privadas (admin, API, portal, cobros y los tres gates de login) devuelven 404 bajo cualquier prefijo de idioma.
- ✓141 tests en tests/i18n-routing-guards.test.ts: cada guarda da el mismo veredicto para /x y /en/x, con casos adversariales (//en/admin, /EN/admin, /e%6E/admin, /english/algo).
Como traductor del sitio (1)
Como traductor del sitio, quiero que falte una clave rompa el build y no la página
- ✓en.ts declarado con satisfies sobre el diccionario español: astro check falla si falta un par.
- ✓tests/i18n-dictionary.test.ts detecta claves huérfanas y valores en inglés idénticos al español.
- ✓src/i18n/format.ts centraliza fechas, números y moneda por locale, con COP explícito para que no se lea como dólares.
Como visitante de /status (1)
Como visitante de /status, quiero que la página no pague una consulta por monitor para pintar la latencia
- ✓recentLatency lee el historial de todos los monitores por lotes, no uno por consulta.
- ✓Índices compuestos en monitor_checks (monitorId, at) y en ci_runs (createdAt), con migración aditiva.
- ✓Cache-Control ajustado en /api/status/latency.
- ✓tests/latency.test.ts cubre el techo de 50 términos por compound SELECT de Turso y los ids duplicados.
Como cliente con lector de pantalla (1)
Como cliente con lector de pantalla, quiero enterarme de lo que cambia solo
- ✓La región aria-live existente anuncia también lo que llega por el digest.
- ✓motion-reduce en la barra de avance.
- ✓Guard de demo: el digest late en modo demo pero leyendo de la base de demo, con casos en tests/portal-demo.test.ts, tests/portal-paths.test.ts y un e2e.
Como visitante anglófono (2)
Como visitante anglófono, quiero que las páginas de producto y estado estén en mi idioma, no solo la portada
- ✓/status, /demo, /log y /paginas-web traducidas y dadas de alta en TRANSLATED_ROUTES.
- ✓Tiempos relativos y fechas por locale, sin cadenas en español coladas en la versión inglesa.
- ✓El formulario de contacto envía el locale y la notificación lo refleja.
- ✓tests/i18n-dictionary.test.ts y tests/i18n-routing.test.ts cubren las claves y rutas nuevas.
Como visitante anglófono, quiero que los proyectos del portafolio no vuelvan al español a mitad de página
- ✓Campos opcionales de título y descripción en inglés en el contenido de la base.
- ✓Helper de localización que cae al español cuando falta la traducción, en vez de dejar el hueco.
- ✓ProjectCard y la página de proyecto localizan título, descripción, estado y avance.
- ✓Variantes inglesas de las imágenes Open Graph.
Como responsable de la documentación (1)
Como responsable de la documentación, quiero que un diagrama mal formado falle en CI y no en la sustentación
- ✓54 tests de geometría y notación en tests/uml-{activity,communication,deployment,component}.test.ts.
- ✓Geometría: nada encimado, ninguna transición atravesando una figura ajena, todo elemento dentro de su nodo o partición.
- ✓Notación: decisiones con al menos dos salidas y todas con guarda, uniones con una sola salida, ningún final con transiciones salientes, ningún nodo inalcanzable.
- ✓/docs/diagrama-componentes rehecho como diagrama de componentes real; un test falla si vuelve a nombrar infraestructura.
Como presentador (1)
Como presentador, quiero proyectar desde cualquier pantalla y controlarla desde mi celular
- ✓Tres vistas sobre una sesión: /present/[sessionId] proyecta, /remote/[sessionId] controla y /[pin] sigue en sincronía.
- ✓El control remoto no carga el iframe del deck: las notas salen de deck_slides, extraídas al subir el archivo.
- ✓Layout de dos mitades sin scroll, botones de 72 px, Wake Lock y vibración corta, todo con degradación silenciosa.
- ✓El deck se sirve desde /decks/[id].html (mismo origen) porque el control por DOM del iframe no funciona contra una URL de blob.
Como público (1)
Como público, quiero seguir la presentación en mi dispositivo escaneando un QR
- ✓PIN de cuatro caracteres, dos letras y dos dígitos, sin i/l/o ni 0/1: 203.136 combinaciones y forma inconfundible con una ruta del sitio.
- ✓/[pin] es la última ruta en resolver; lo que no tiene forma de PIN devuelve el 404 normal sin llegar a consultar Redis.
- ✓Generación con reintento contra rutas reservadas y PIN vivos; un test cruza la lista de reservadas contra los archivos de src/pages.
- ✓Vista de solo lectura: sin controles, sin notas y con el teclado del deck neutralizado dentro del iframe.
Como responsable del coste (3)
Como responsable del coste, quiero tiempo real sin una invocación abierta por espectador
- ✓El público se suscribe DIRECTAMENTE al bus de Upstash con un token de solo lectura; Vercel solo trabaja al crear la sesión, al dar el snapshot y en cada comando.
- ✓Dos bases separadas: estado (privada, con el secreto del presentador) y bus (token público, solo números de slide).
- ✓Tres capas de sincronía: bus, resincronización cada 10 s y polling de rescate - pub/sub no garantiza entrega.
- ✓El servidor es la fuente de verdad: valida rango, persiste y reemite; el cliente descarta mensajes con versión anterior.
- ✓tests/present-sync.test.ts levanta dos clientes y comprueba que el salto llega a ambos, en orden, y que sin secreto no se mueve nada.
Como responsable del coste, quiero que una página pública lea lo mismo el primer día que el año siguiente
- ✓Tabla monitor_daily con un resumen precalculado por monitor y día, alimentada por un cron nuevo (/api/cron/monitor-rollup); /status lee ~720 filas de resumen más el día en curso en vez de agregar los 90 días crudos de monitor_checks.
- ✓Medido contra la sqld local: 5.912 filas recorridas por render antes, 552 después.
- ✓Los contadores del resumen son exactos y aditivos; la latencia no, porque un percentil no se suma, así que cada día guarda un histograma de 32 cubos del que sale el p95 de la ventana, dentro del 7,4% del exacto que devolvía la window function.
- ✓La escalera de cubos es geométrica y no lineal: en un percentil importa el error relativo, y la primera versión lineal pintaba 2450 ms un p95 real de 2030.
- ✓tests/monitor-rollup.test.ts cubre histograma, percentil y agregación por día UTC.
Como responsable del coste, quiero que una prueba de carga no pueda alcanzar la base real
- ✓El health check declara contra qué base está sirviendo el objetivo, y los perfiles de k6 (carga y estrés) se niegan a arrancar un solo usuario virtual si esa base no es local.
- ✓La guarda vive en el arranque de la prueba, no en la documentación: apuntar a localhost no basta cuando el dev server detrás puede estar conectado a Turso.
- ✓tests/db-target.test.ts cubre la detección de base local.
Como aprendiz (4)
Como aprendiz, quiero registrar cada sesión de práctica y ver si el hábito se sostiene
- ✓Registro de sesión con día, minutos, tema, bitácora y hito relacionado; tres tablas nuevas (skill_tracks, skill_sessions, skill_milestones) en migración aditiva.
- ✓Racha con día de gracia: si la última sesión fue ayer sigue viva y se marca "en juego", en vez de romperse a las 00:00 de un día que apenas empieza.
- ✓Meta semanal configurable con lo que falta y el ritmo por día necesario para llegar antes del domingo.
- ✓Mapa de calor de 26 semanas con intensidad relativa al mejor día de la ventana, no a umbrales fijos.
Como aprendiz, quiero un temario concreto en vez de una intención vaga de "aprender .NET"
- ✓Temario semilla de 28 hitos en 8 áreas, de fundamentos de C# a desplegar una API real, con dos hitos de proyecto insignia.
- ✓La siembra es idempotente por título: volver a dispararla añade los hitos nuevos de la plantilla sin duplicar ni pisar el estado de los cerrados.
- ✓Los hitos se editan desde el panel una vez sembrados; la plantilla en src/data/track-dotnet.ts no vuelve a tocar un track existente.
Como aprendiz, quiero calcular las fechas de mi etapa productiva a partir de cuándo empiezo
- ✓src/lib/sena-ep.ts (módulo puro e isomorfo, sin node:crypto ni ../db) calcula computeHitos(tipo, inicioIso): inicio, visita de concertación a los 15 días, una bitácora por mes durante los 6 meses que dura la etapa productiva en los dos niveles (864 horas de diseño curricular, igual para técnico y tecnólogo), visita parcial de seguimiento y cierre 5 días después del fin nominal.
- ✓La calculadora en /ep exporta los hitos a .ics para importarlos al calendario del aprendiz y genera un enlace compartible con los parámetros en la URL. Cada evento lleva VALARM a 7 y 1 día: el recordatorio lo dispara el calendario del propio aprendiz, sin pedirle el correo ni guardar nada de un visitante.
- ✓src/lib/festivos-co.ts (módulo puro) calcula los 18 festivos colombianos del año - fijos, los que la Ley 51 de 1983 corre al lunes, y los móviles respecto al Domingo de Pascua por el algoritmo gregoriano anónimo - y marca los hitos que caen en fin de semana o festivo proponiendo el último día hábil. Cubierto por tests/festivos-co.test.ts contra fechas conocidas de Pascua y traslados Emiliani.
- ✓La página tiene hoja de impresión propia (@media print, is:global porque alcanza navbar y footer): oculta cromo, barra lateral y formularios, fuerza la paleta a tinta sobre papel y deja los checkbox de entrega vacíos para marcar a mano. El botón Imprimir / PDF existía desde antes sin estilos de impresión.
- ✓Un botón "Crear mi cronograma" lleva al formulario y deja el foco en la fecha. La ficha de la página sigue siendo la de referencia: el cronograma de otro aprendiz se calcula, no reemplaza el contenido por defecto.
- ✓'ep' añadido a RESERVED_ROOT_SEGMENTS: Astro ya resuelve las estáticas antes que [pin].astro, así que el riesgo real es el inverso, que un PIN generado coincida con esta ruta y el público que escanee el QR aterrice en la página de etapa productiva en vez de en el deck. tests/present-reserved.test.ts falla si alguien añade una ruta raíz sin registrarla.
Como aprendiz, quiero un correo cuando se acerca un hito sin tener que volver a la página
- ✓src/pages/api/admin/sena-recordatorio.ts (bajo sesión admin: uso personal, nunca recolecta el email de nadie más) crea, consulta o cancela la única suscripción activa (tipo, inicio, diasAntes).
- ✓src/pages/api/cron/sena-recordatorio.ts recalcula hitos con computeHitos, filtra los que caen dentro de la ventana con hitosPorAvisar, y envía un único correo con sendEmail (notify.ts) al ALERT_EMAIL_TO ya configurado, nunca a un correo del request.
- ✓notifiedKeys evita reavisar el mismo hito: cada clave es título+fecha, persistida tras un envío exitoso.
- ✓Fail-open como el resto de los crons de observabilidad: un fallo se registra en logs y responde 200 en vez de tumbar el cron.
Como responsable de que el dato sea correcto (1)
Como responsable de que el dato sea correcto, quiero que la racha no dependa de la zona horaria del servidor
- ✓Los días se guardan como clave de calendario "YYYY-MM-DD" en zona de Bogotá, no como timestamp: el servidor corre en UTC y una sesión de las 8 de la noche caería en el día siguiente, partiendo la racha.
- ✓Los logros se derivan de las sesiones en cada render en vez de persistirse: una tabla de badges se desincroniza al borrar una sesión mal registrada.
- ✓Cada logro queda fechado en el día en que se cumplió (cruce del umbral acumulado, cierre de la racha), no en el día en que se consulta el panel.
- ✓tests/skills.test.ts: 31 casos sobre el módulo puro, incluidos cambio de mes, año bisiesto, fin de año, corte de racha a los dos días y niveles relativos del mapa de calor.
Como dueño del sitio (1)
Como dueño del sitio, quiero poder mostrar el avance sin publicar mi bitácora
- ✓Visibilidad por track (is_public, privado por defecto); /certifications publica solo el agregado: horas, porcentaje del temario y mejor racha.
- ✓La bitácora, las fechas de cada sesión y los hitos individuales no salen del panel, misma regla de OPSEC que /status.
- ✓Un track recién sembrado y sin sesiones no aparece en público: cero horas y cero por ciento no dicen nada bueno.
- ✓Sección traducida en los dos diccionarios (es/en), que es lo que exige la paridad de claves de npx astro check.
Como capacitador (1)
Como capacitador, quiero que el material siga sirviendo cuando termina la sesión
- ✓Tres tablas nuevas en migración aditiva: training_programs (catálogo), training_resources (banco) y training_access_codes (grupos).
- ✓Banco público en /capacitacion con detalle por recurso; el contenido es markdown en base de datos, renderizado en el servidor con un subconjunto propio que escapa el HTML antes de formatear.
- ✓Las presentaciones no se duplican: un recurso de tipo deck referencia la tabla decks, que sigue siendo la única fuente de verdad del material proyectable.
- ✓La previsualización del panel usa el mismo renderizador que la página pública, para que no sea una previsualización que miente.
Como dueño de los datos (1)
Como dueño de los datos, quiero saber si el backup diario realmente se está haciendo
- ✓Descubierto con el store de Blob vacío: un mes sin un solo archivo. Dos fallos que se tapaban entre sí: el endpoint vivía bajo /api/admin/, donde el middleware exige sesión y el cron se llevaba un 302; y el handler del backup era POST mientras que los crons de Vercel disparan GET, con lo que la petición diaria caía en el GET de "listar backups" y devolvía 200.
- ✓El backup se mueve a /api/cron/backup con Bearer CRON_SECRET, y es el único cron del repo que NO es fail-open: devuelve 500 cuando falla, porque un cron que solo puede responder 200 no está verificado, está silenciado.
- ✓tests/crons.test.ts recorre cada entrada de vercel.json y afirma las cuatro condiciones que fallaban: la ruta existe, exporta GET, comprueba CRON_SECRET y no cuelga del gate de /api/admin. Ninguna necesita desplegar para comprobarse.
- ✓La ruta del cron queda vetada en la demo, cubierto por tests/demo.test.ts.
Como responsable del incidente (1)
Como responsable del incidente, quiero un procedimiento escrito para la próxima vez que la cuota se agote
- ✓docs/runbook-cuota-turso.md documenta qué pasó (86.000 filas leídas por llamada a /api/now.ts pese a devolver un único número), la regla que lo evita (ningún agregado público sobre ventanas >24h sin rollup) y que es la tercera vez que el mismo patrón agota la cuota.
- ✓Dos caminos de recuperación documentados: aguantar hasta el corte del ciclo (la capa de respaldo de RNF-27 se apaga sola) o mudar a una base nueva, con el requisito de que ENCRYPTION_KEY debe coincidir para no perder la bóveda.
- ✓/api/now.ts (error budget) y /api/engineering/live.ts pasan a leer de monitor_daily en vez de agregar monitor_checks/web_vitals crudos; este último además cambia no-store por s-maxage=30.
Como quien prueba el modo respaldo (1)
Como quien prueba el modo respaldo, quiero historial de monitoreo sin esperar un incidente real
- ✓scripts/seed-monitor-history.mjs genera sondeos en memoria y los pasa por aggregateChecks, el mismo módulo real de rollup que usa el cron en producción: el histograma queda con los mismos cubos.
- ✓90 días de monitor_daily (~900 filas) pero solo 2 días de monitor_checks (~5.700): sembrar 90 días de crudo para alimentar los 40 puntos de la mini-gráfica EKG habría repetido el mismo error de volumen que causó el incidente.
- ✓El azar es determinista (derivado de monitor+instante): dos corridas producen el mismo p95, y el perfil de latencia no se realimenta de monitors.last_response_ms para no derivar entre pasadas.
- ✓scripts/restore-backup.mjs repuebla las tablas de negocio desde el último backup de Blob con --dry-run obligatorio antes de escribir, y se niega a pisar tablas con filas salvo --forzar.
Como visitante en modo respaldo (1)
Como visitante en modo respaldo, quiero que /status y el poll no sigan trabajando cuando no los veo
- ✓El poll de /status (30s) y el de /docs/pipeline-en-vivo (6s → 15s) se detienen con la pestaña oculta (Page Visibility API).
- ✓DESTINOS_RESPALDO deja deliberadamente FUERA los tres servicios pausados a mano en Vercel (DEPLOYMENT_PAUSED) mientras dura el ahorro de cuota: sondearlos los pintaría "Caído" durante semanas por una decisión operativa, no por una avería, y un panel de estado que reporta como incidente algo que su dueño apagó a propósito enseña a ignorarlo.
Como quien entrega evidencia (1)
Como quien entrega evidencia, quiero explicar qué log produce cada reporter antes de citarlo
- ✓/docs/reportes-pruebas documenta los 7 reporters de Vitest (default, verbose, dot, tap-flat, junit, json, html) con salida real capturada de tests/phone.test.ts, no reescrita, incluida la anatomía de un fallo provocado a propósito.
- ✓Tabla de equivalencias con el instrumental de Java (Surefire, JaCoCo, PIT), porque el vocabulario estándar de pruebas viene de Maven y los nombres coinciden solo a medias.
- ✓Documentado con el error literal que devuelve el reporter html si falta la dependencia @vitest/ui.
Como responsable del LAB (1)
Como responsable del LAB, quiero los hallazgos de las corridas de k6 reflejados donde se sustenta
- ✓RF-505 actualizado con los hallazgos H-01 a H-05 de la corrida de referencia: el colapso es de concurrencia, no hay margen amplio entre nivel sano y roto, y sin ventana de enfriamiento la recuperación puede no completarse en 120s.
- ✓lab/k6/estres.js deja de falsear "CPU 0%" cuando el propio endpoint de muestreo (proceso.ts) se satura bajo la prueba; ahora distingue explícitamente "sin dato".
- ✓Prerendering habilitado en páginas estáticas de /docs para alinear el comportamiento de build con la realidad de producción.
Como autor del modelo (1)
Como autor del modelo, quiero que un flujo no pueda saltarse un perímetro sin que el test lo note
- ✓tests/red.test.ts verifica geometría (zonas sin solaparse, host dentro de su zona, ninguna traza atravesando una zona ajena a sus extremos, rótulos sin encimarse) y notación (todo flujo con protocolo, todo cruce con puerto y al menos un control, ningún host aislado).
- ✓Regla dura verificada con tres pruebas hostiles a propósito: un flujo que se salta el perímetro, un cruce sin control y un host huérfano, exigiendo que el verificador los detecte.
- ✓findLayoutIssues corregido para usar bboxZona (la caja real, incluida la cabecera de la zona) tras encontrar cabeceras encimadas que la primera versión no detectaba.