168 historias en formato XP ("Como <actor>, quiero <acción> para <beneficio>"), consolidadas desde el kanban del proyecto (151 aceptadas de 168). 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 (31)
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 visitante, quiero ver qué corre solo en este sistema y si su última corrida salió bien
- ✓El catálogo entero vive en src/data/automatizaciones.ts: la página no escribe a mano ni un horario ni un nombre de workflow (RNF-14). Tipado, así que astro check rompe si una entrada queda incompleta.
- ✓Una sola petición a la API de GitHub (40 corridas de main) reducida en memoria a la última de cada workflow. Pedir el último run workflow por workflow serían seis peticiones contra la cuota anónima de 60/hora, y esta página la puede abrir cualquiera.
- ✓Fail-open por separado en las dos fuentes vivas: si GitHub agota cuota o la base no contesta, la sección se pinta con lo que hay y lo dice. Una página de observabilidad que devuelve 500 cuando lo observado falla es inútil justo cuando más se la mira.
- ✓Mismo criterio OPSEC de /status y /security: se describe qué hace cada automatismo y qué se pierde si deja de correr, nunca umbrales de bloqueo, rutas señuelo ni nombres de reglas de detección.
- ✓automatizaciones entra en RESERVED_ROOT_SEGMENTS: es una ruta de un segmento en la raíz y competía con el espacio de los PIN de presentación. Lo cazó el test del repo, no una revisión.
- ·Alta en el sitemap y cascarón en /en. La página ya está enlazada en el pie y su texto sale del diccionario, pero mientras no esté en TRANSLATED_ROUTES no debe anunciarse en inglés.
Como visitante, quiero entender el criterio detrás del simulador sin tener acceso al panel
- ✓Nota bilingüe "Unos proveedores te cobran, otros te cortan" en /notes: la tesis del tope duro contra el excedente, apoyada en los dos incidentes reales de cuota y no en un anuncio de feature.
- ✓SIN CIFRAS DE TARIFAS A PROPÓSITO. Las cuotas cargadas en el simulador son una estimación con fecha de verificación, y un precio mal citado en una página pública envejece fatal. La nota habla del método, que es lo que no caduca.
- ✓El caso 02 de /tools (Costos y P&L) gana un detalle sobre el simulador en los dos diccionarios, en vez de un caso nuevo: es la misma pregunta de negocio, no un producto aparte.
- ·El caso propio en /tools con su mock visual, si alguna vez merece tarjeta independiente.
Como visitante, quiero que la portada se sienta como un instrumento vivo y no como una plantilla de portafolio
- ✓Terreno de isolíneas en WebGL2 directo: el cursor levanta un pico, el clic emite un pulso y el scroll lo aplana hasta continuar en la cinta de señales (RF-022).
- ✓Índice cinético de proyectos con portadas generativas, proceso como pipeline de CI, expertise con pantallas animadas y la huella del visitante en la sección del laboratorio.
- ✓tests/motion-portada.test.ts (13 casos): el rayo del cursor es el inverso exacto de la proyección del shader, semillas deterministas y huellas siempre plausibles.
- ✓Falla abierto: sin WebGL2 queda el halo de CSS, un error del motion devuelve la visibilidad de lo escondido y el terreno baja su resolución si los fotogramas se alargan.
Como visitante, quiero que /engineering mida mi propia visita delante de mí
- ✓Osciloscopio de la visita con las fases de la carga y las marcas de TTFB, FCP y LCP, ubicada frente a la distribución real ("más rápida que el X % de las visitas") (RF-023).
- ✓No se envía nada nuevo: la comparación se hace en el navegador contra 21 cuantiles por métrica, sin muestras.
- ✓De paso: la disponibilidad de 90 días leía monitor_checks crudo en cada render y ahora lee monitor_daily; los paneles de detalle ya no desbordan en móvil.
- ✓tests/motion-engineering.test.ts (27 casos) y capturas a 1440x900 y 390x844 en español e inglés.
Como visitante, quiero ver a /security defender el sitio y no solo leer que lo hace
- ✓Filtro de capas: cada petición termina donde el sistema la dejaría de verdad, todas grises hasta el clasificador (RF-024).
- ✓Hallazgos corregidos como una línea de commits reales que se cierran al bajar, incluidos los 9 de la auditoría del 22 al 24 sep.
- ✓LECTURAS ACOTADAS: los agregados se recalculan como mucho cada 3 h y se guardan en app_settings; antes eran cinco consultas sobre security_events crudo en cada render.
- ✓Sin consultas nuevas ni datos nuevos publicados: la OPSEC no cambia. tests/motion-security.test.ts (14 casos) y tests/security-vitrina-db.test.ts (8 casos).
Como visitante, quiero ver las herramientas del panel trabajando y no siete maquetas quietas
- ✓Circuito de operación con cuatro escenarios en bucle (caos, deploy roto, sondeo hostil, proyecto nuevo) y una ventana fija con la escena del caso que se lee (RF-025).
- ✓Las cifras de las escenas salen del código real: monthlyEquivalent, projectPnL, computeSloFromCounts, detectSpikes y cifrado AES-256-GCM hecho en el build.
- ✓Si un byte alterado llegara a descifrarse, el build falla en vez de publicar una demostración falsa.
- ✓tests/motion-tools.test.ts (17 casos); la página sigue prerenderizada.
Como visitante, quiero ver cada experimento del laboratorio ocurrir sobre la pasarela de pagos
- ✓Banco de ensayo con las cinco defensas de la pasarela: la decisión de cada transición la toma canTransition de payments-state.ts importada tal cual (RF-026).
- ✓Pista de CI por corrida, ciclo de hallazgos y escalera de carga con los escalones reales de la última corrida de estrés.
- ✓tests/motion-lab.test.ts (21 casos): con una máquina de estados permisiva el webhook tardío deja de superarse, lo que prueba que el guion depende del código real.
Como visitante, quiero ver en /log cómo se suman las horas de trabajo
- ✓Reloj de sesiones de 30 días por 24 horas: las cifras de arriba se suman con lo que se dibuja y el navegador llega al mismo número que el servidor (RF-028).
- ✓DATO NUEVO PUBLICADO, decidido explícitamente: la hora de cada commit privado redondeada a 5 min, sin repositorio ni mensaje.
- ✓De paso: el gráfico por día y la racha contaban días en UTC; ahora van en hora de Bogotá.
- ✓tests/motion-log.test.ts (23 casos).
Como visitante, quiero ver mi huella leerse en dos tiempos y reconocerse cuando vuelvo
- ✓La huella se lee primero con lo que cualquier sitio ve sin pedir nada, y solo tras el consentimiento se suman canvas, audio y fuentes (RF-030).
- ✓En el tablero, cada dispositivo trae su huella en SVG y al volver se marca "reconocido de nuevo"; el visor se comparte con la portada (HuellaVisor.astro).
- ✓Comprobación de defensas con cuatro dispositivos de ejemplo, ilustrativa y declarada como tal.
- ✓tests/motion-fingerprint.test.ts (23 casos).
Como visitante, quiero encontrar en octubre unos fantasmas en la portada sin que la página cambie ni se ponga lenta
- ✓Seis fantasmas (tres en móvil) dibujados como curvas de nivel, en blanco violáceo y violeta, que cruzan despacio por detrás del lienzo y del velo del texto (RF-031).
- ✓Segunda pasada a pedido del usuario ("están extremadamente sencillos"): anillos que respiran, cola que ondea, parpadeo; miran al cursor, huyen de él, se acercan curiosos con el cursor quieto y se asustan con el clic; y un fantasma tímido que solo se asoma con el puntero quieto.
- ✓Capa aparte en src/components/Fantasmas.astro, montada desde BaseLayout solo en la portada: hero, shader, navbar y footer idénticos a como estaban.
- ✓Sin filtros animados; un solo bucle de reacciones que se detiene fuera de pantalla y solo escribe transform, opacity y dos variables. Fotogramas del hero medidos con el cursor en movimiento, con y sin la capa, sin diferencia apreciable.
- ✓Con movimiento reducido quedan quietos y tenues; sin desbordamiento horizontal en móvil.
- ·Aceptación del usuario sobre el sitio en producción.
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 (9)
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 cliente, quiero ver mi proyecto, ajustar lo que me dejen y aceptarlo desde un enlace
- ✓El proyecto dibujado como un plano: habitaciones construidas o punteadas, que se construyen al tocarlas y mueven el precio (RF-217).
- ✓Perillas recalculadas en el servidor desde la versión enviada; la vista no lleva citas, horas ni el rango interno.
- ✓Aceptación con constancia y rechazo si el total no coincide con el que vio el cliente; anticipo idempotente con Wompi.
- ✓Capturas en escritorio y móvil sin desborde.
Como cliente, quiero ver mi propuesta, aceptarla y aprobar adicionales desde un enlace
- ✓Token de 128 bits guardado como huella y cifrado; aceptación con la huella de lo que se vio y monto en el WHERE (RF-224).
- ✓Constancias SHA-256 recalculables y aviso por ntfy en cada decisión.
- ✓tests/cotiza-enlace.test.ts (14 casos) y recorrido real en celular: acepta, aprueba y el enlace no abre el panel.
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 (5)
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 desarrollador, quiero que la puerta de los crons se pruebe una vez y valga para los nueve
- ✓cronSecretOk en src/lib/cron-auth.ts sustituye la comprobación copiada en cada handler: timingSafeEqual sobre longitudes iguales, y rechazo si no hay secreto en el entorno.
- ✓tests/crons.test.ts cubre ahora las dos capas: que toda ruta declarada en vercel.json existe, exporta GET y comprueba el secreto (RNF-28), y los seis casos de la propia función, incluido el header de cualquier longitud, que es donde timingSafeEqual lanzaría.
Como desarrollador, quiero que el cableado del navegador esté probado y no solo la aritmética
- ✓e2e/infra.spec.ts entra por el pase de demo y comprueba lo que Vitest no puede ver: que la página abre sin errores de JS, que cambiar de escenario y editar una celda recalculan, y que subir Turso de plan convierte un tope duro en un excedente facturable.
- ✓LOS CLICS VAN CON force:true Y NO ES PARA TAPAR NADA: bajo Chromium headless el panel no produce frames de animación (medido: requestAnimationFrame a 0/s en /admin, /admin/costs y esta página, frente a 2-4 en las públicas), y el chequeo de estabilidad de Playwright compara la caja entre dos frames consecutivos, así que el clic expira a los 30 s con el botón visible y quieto. force salta ese chequeo pero sigue disparando un clic de ratón real: un overlay de verdad seguiría robándose el evento.
- ✓Es el primer spec que pulsa algo DENTRO del panel; los seis anteriores solo comprobaban redirecciones y leían contenido. Por eso el problema no había salido antes, y por eso queda documentado en la cabecera del spec.
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 (4)
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 asistente, quiero ver la página que el ponente está tocando y no el formulario de entrada
- ✓LO QUE VIAJA ES LA URL, NO EL DOM. Duplicar el árbol entre N navegadores es otro problema y otro orden de fragilidad, y no hace falta: todo resultado en esas tres páginas pasa por una navegación. Lo que se pierde es el tecleo carácter a carácter, que además enseñaría cuántas letras tiene una contraseña.
- ✓FUNCIONA PORQUE NO HAY SESIÓN QUE ESPEJAR: al portal se entra por /api/portal/demo, un GET sin login contra la base de demo, así que quien abra esa URL ve lo mismo sea el ponente o cualquiera de la sala. Con un login real no funcionaría, y por eso es una regla del runbook y no un detalle de implementación.
- ✓Solo http: y https:. Un javascript: en un location.replace sería ejecución de código venida de la red, y ninguna página del mazo lo necesita. Ante cualquier duda se devuelve null: quedarse sin espejo es una degradación, aplicar una URL rota es una pantalla en blanco delante del tribunal.
- ✓Orden por seq y URL atada a su diapositiva por pos, en src/lib/presentacion/espejo.ts: 19 casos.
Como asistente, quiero un enlace que reúna la documentación del proyecto cuando la sustentación ya terminó
- ✓/presentacion-end es página pública, indexable y con la navegación normal del sitio: se manda DESPUÉS, no se proyecta, y no toca el sistema de beats.
- ✓El corte de "en números" se cuenta en vivo sobre el kanban de este mismo archivo filtrando por columna aceptada, no son cifras escritas a mano: si el tablero crece, la página no se toca.
- ✓Traducida en los tres pasos del repo: texto al diccionario, cascarón en src/pages/en/ y alta en TRANSLATED_ROUTES.
- ✓LA RUTA TUVO QUE ENTRAR EN RESERVED_ROOT_SEGMENTS y no estaba: es un segmento en la raíz y competía con el espacio de los PIN de presentación, así que un PIN generado podía coincidir y mandar a quien escaneara el QR a esta página en vez de a su deck. Lo cazó tests/present-pin.test.ts, que es la red que existe justo para esto.
- ·Los enlaces definitivos de documentación: los de hoy son un marcador razonable. Al llegar se tocan DOS sitios a la vez (DOCS_META y presentacionEnd.docs en es.ts y en.ts) o el test de paridad de diccionarios los agarra.
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 (2)
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 sustentante, quiero que la corrida de carga se lea en el panel y no en un archivo JSON del repo
- ✓SE PARSEA EL RESUMEN DEL SCRIPT, NO EL JSON CRUDO DE K6: el crudo pesa cientos de KB, cambia de forma entre versiones y mezcla las métricas de la rampa con las de la meseta, que es justo lo que los scripts ya habían separado. El parser vive en src/lib/lab/load-test.ts, sin BD ni red.
- ✓La tabla guarda dos cifras que ningún percentil agregado responde: capacidad sostenida y punto de quiebre. La página las pinta con la escalera en SVG inline, barras de p95 coloreadas por estado y p50 como línea, sin librerías.
- ✓"NO SE RECUPERÓ" Y "SE RECUPERÓ AL INSTANTE" SE DISTINGUEN: los dos podrían imprimirse como un número pequeño y significan lo contrario. Un tramo sin peticiones tampoco cuenta como recuperación, que era el modo de fallar del hallazgo H-04 (una muestra en cero leída como sistema sano).
- ✓El escenario de carga mide en VUs y el de estrés en req/s, así que la tarjeta lee el quiebre de los escalones y no del campo en req/s: una corrida de carga que sí se rompió mostraba "no llegó", que es exactamente lo contrario de lo que pasó.
- ✓21 tests sobre los JSON de corridas REALES de lab/k6/resultados/, no sobre fixtures inventados: si el formato del resumen cambia, los tests se enteran.
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 (4)
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 presentador, quiero que un toque avance exactamente una diapositiva, también en la portada y en el cierre
- ✓EL MAZO NO SON LOS BEATS. El bundle solo sabe contar beats y su contador dice "01 / 19" tanto en la cita como en la portada: derivar la posición de ahí colapsaba cuatro diapositivas reales en dos números, un toque gastaba tres flechas al arrancar y el cierre era inalcanzable porque el servidor acotaba contra un total que no lo incluía.
- ✓El arreglo es un reparto de mando explícito: el bundle manda en los beats (por teclas) y la pantalla manda en las capas (por estilo). Las reglas viven en src/lib/presentacion/mapa.ts como índice global sobre intro, beats y outro, con 21 casos sin DOM ni bundle.
- ✓Ni un número del bundle cableado: ni cuántos beats trae, ni cuántas capas, ni sus z-index. Solo se asume su forma, que es la de cualquier mazo. Es lo que permite reexportarlo sin tocar el código.
- ✓El KeyboardEvent se construye con el constructor DEL IFRAME. Uno creado en este realm y despachado contra el iframe no dispara su listener y la diapositiva sencillamente no se movía: costó una depuración entera y queda escrito donde se lee.
Como presentador, quiero que el mando me diga dónde está la presentación de verdad, no que el servidor recibió mi toque
- ✓Estado partido en dos claves con un escritor cada una: destino lo escribe el mando (una intención) y actual lo publica la pantalla (un hecho). Sin CAS en el almacén, una sola clave compartida podría perder un toque justo en el instante en que la pantalla publica, que es exactamente cuando se vuelve a pulsar.
- ✓La posición es ABSOLUTA, no una cola de comandos: un sondeo perdido no pierde nada porque el siguiente trae el destino entero.
- ✓El mando pinta la posición real y el destino solo mientras no se ha alcanzado. La versión anterior pintaba "ok" con el 200 del POST, que solo confirma que el servidor apuntó la intención: en el final del mazo era un botón que respondía bien mientras nada se movía.
- ✓Frescura de 15 s para distinguir "la charla lleva rato en la misma diapositiva" de "la pestaña de la pantalla se cerró". Reglas de acotado y adopción en src/lib/presentacion/estado.ts, 12 casos.
- ✓Sondeo una vez por segundo y solo con la pestaña visible: el teléfono pasa la charla bloqueado en el bolsillo y no tiene sentido gastar lecturas del almacén para nadie. Estado en Redis con TTL de 6 h, nada en Turso.
Como presentador, quiero teclear la URL del mando de memoria y que no me mande a un login
- ✓El mando vive en /remote a secas, la ruta más corta que quedaba libre en la raíz, y es pública a propósito: una cookie de OAuth caducada y sin wifi minutos antes de empezar es peor riesgo que una URL cuyo peor abuso es pasar una diapositiva de algo que ya está proyectado en la pared.
- ✓El vecindario obliga a una excepción por ruta EXACTA en el matcher isAdmin: /remote y /remote/ quedan fuera del gate, pero cualquier /remote/<sessionId> (el control de las presentaciones con deck y PIN) sigue exigiendo sesión de administrador. Fijado en e2e/auth.spec.ts para que nadie lo relaje por descuido.
- ✓presentacion entra también en RESERVED_ROOT_SEGMENTS, por el mismo choque con el espacio de PIN que /automatizaciones.
- ✓no-store y noindex explícitos en el mando y en la pantalla.
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 (5)
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 aprendiz, quiero saber cuánto me deben pagar en la etapa productiva y revisar que mi contrato lo diga
- ✓Sección Remuneración en /ep (entre "Cómo funciona la etapa" y Bitácoras, con ancla desde "Alternativas vigentes"): marco de la Ley 2466 de 2025, porcentaje del apoyo por modalidad, auxilio de transporte o de conectividad, prestaciones y un checklist de revisión del contrato con el mismo patrón persistido que el de cierre.
- ✓src/lib/sena-remuneracion.ts (módulo puro e isomorfo) calcula el mes de arranque partido día a día (pensión solo sobre los días de práctica, salud sobre el mes completo), el mes típico, la prima por semestre, cesantías e intereses por año con su fecha legal o la liquidación, vacaciones compensadas y el total de la etapa. En dual, el paso al 100 % cae en el aniversario del contrato aunque sea a mitad de mes.
- ✓SMMLV y auxilio por año en src/data/salario-minimo-co.ts, no en el componente. Un año sin decreto (2027) reutiliza el último conocido y el desglose lo marca como provisional, porque la etapa de la ficha cruza el cambio de año.
- ✓tests/sena-remuneracion.test.ts: 21 casos con montos calculados a mano (mes partido, cruce de año con y sin decreto, febrero como mes comercial completo, aniversario dual, fechas inexistentes).
- ·Cargar SMMLV y auxilio de 2027 cuando salgan los decretos de diciembre de 2026 (TODO en salario-minimo-co.ts).
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.
Como emisor (3)
Como emisor, quiero una cuenta de cobro que contabilidad no me devuelva
- ✓src/lib/cuentas-cobro.ts es puro e isomorfo (sin ../db ni node:crypto): el formulario lo importa también en el navegador para previsualizar el neto mientras se teclea, igual que cobros.ts en /cobrar. 71 tests sin base de datos.
- ✓numeroALetras con tabla de casos por las irregularidades reales del español: 21 es veintiuno pero 21.000 es veintiún mil, 100 es cien y 101 ciento uno, 500 quinientos, 700 setecientos, 900 novecientos. Como el número siempre va seguido de "pesos", la apócope se aplica siempre y eso simplifica la función entera.
- ✓validateCuentaCobro exige cédula, dirección y datos bancarios del emisor, NIT del deudor, concepto detallado y ciudad. No es burocracia del formulario: son los campos del Documento Soporte del pagador. Los faltantes se muestran TODOS a la vez, en la página y en el 422 de la API, para no caer en el ciclo de arreglar uno y descubrir el siguiente.
- ✓El PDF se dibuja con pdf-lib y no HTML→PDF (no hay Chromium en la función serverless). Todo texto pasa por un saneador WinAnsi: las fuentes estándar LANZAN en runtime con un emoji pegado desde WhatsApp o unas comillas tipográficas, y ese fallo no aparece en compilación. Cubierto con descripciones sucias a propósito.
- ✓Vetada en modo demo por patrón en lib/demo.ts: el PDF imprime cédula, dirección y número de cuenta, y es un GET, así que "la demo es solo lectura" no lo habría detenido. Mismo hallazgo que en su día cambió el diseño de la demo con los reveladores de la bóveda.
Como emisor, quiero que reimprimir un documento viejo no lo actualice con mis datos de hoy
- ✓Al emitir se congelan emisor, deudor y retenciones en la propia fila (issuer_snapshot, payer_snapshot, retentions). A partir de ahí el PDF no vuelve a leer app_settings ni clients: cambiar de banco en marzo no reescribe lo entregado en enero. Es el bug más silencioso del módulo y tiene test propio.
- ✓Serie CC-AAAA-NNN independiente de la INV- de las facturas, con UNIQUE en base y reintento. Bug encontrado al probar la carrera: el detector de colisiones comparaba String(e), pero drizzle envuelve el error del driver y su message es solo "Failed query: insert into…"; el texto del constraint vive en la cadena de causas, así que el reintento no se disparaba nunca. esConflictoUnique recorre esa cadena, y la misma corrección arregla de paso la numeración de las facturas del portal, que arrastraba el mismo defecto.
- ✓Una cuenta emitida, pagada o anulada no acepta cambios: corregirla se hace anulando y emitiendo otra, no con un UPDATE. Verificado con libSQL en archivo temporal, nunca :memory:.
- ✓Las tarifas se recalculan en el momento de emitir y no se confía en lo guardado en el borrador: entre la última edición y la emisión pudo cambiar una tarifa en Ajustes.
Como emisor, quiero saber antes que mi contador cuándo dejo de poder usar este documento
- ✓Semáforo de las 3.500 UVT del art. 437 par. 3 ET en /admin/cuentas-cobro: aviso al 70 %, alerta al 90 % y advertencia explícita al superarlo, momento en que se pasa a facturación electrónica obligatoria y este documento deja de ser el soporte correcto.
- ✓Cuenta lo emitido y no lo cobrado (el tope mira ingresos del año); borradores y anuladas quedan fuera por no ser documentos vivos.
- ✓Bug encontrado al probarlo: la comparación de fechas iba en una plantilla sql cruda, donde drizzle no conoce la conversión de la columna timestamp y ataba el Date con otra unidad. Devolvía cero en silencio, que es justo el modo de fallo peligroso en un semáforo de cumplimiento. Corregido con gte/lt sobre la columna.
- ✓Tarifas, bases en UVT, UVT y SMMLV en app_settings y no en el código: durante 2025-2026 se movieron dos veces con el Decreto 572 de 2025 y las fuentes públicas aún se contradicen entre 2 y 4 UVT para servicios.
- ·Validación de la parametrización con un contador antes de emitir la primera cuenta de cobro real.
Como operador (11)
Como operador, quiero enterarme de que un cron dejó de dispararse sin esperar a echar en falta su efecto
- ✓Tabla cron_runs (job, ok, duration_ms, detail, created_at) y conRegistro() envolviendo los nueve handlers de /api/cron/*. El precedente que lo justifica es real: los sondeos de monitores se cortaron tres semanas de 2026 y el hueco apareció mirando el historial, no por una alerta.
- ✓Fail-open: si el insert falla (base caída, cuota agotada) el cron devuelve lo que iba a devolver. Un registro que puede tumbar la tarea que observa es peor que no tener registro.
- ✓Se awaita, al contrario que recordSecurityEvent: aquí no hay un usuario esperando la respuesta y la función serverless puede morir después del return, así que el fire-and-forget perdería justo las filas de los crons que peor terminan.
- ✓Los 401/403 NO se registran. /api/cron/* es público y recibe escaneo constante: anotar los rechazos llenaría la tabla de ruido y enterraría lo único que se quiere ver, que es el calendario real.
- ✓El detalle se recorta a 300 caracteres y nunca lleva cuerpos de respuesta ni secretos: esa tabla la lee una página pública. Índice por created_at, porque Turso factura filas escaneadas y no devueltas (RNF-25).
Como operador, quiero saber cuánto costaría mi stack si el tráfico creciera, y no solo lo que pago hoy
- ✓UN EXCEDENTE Y UN TOPE DURO NO SON EL MISMO PROBLEMA y por eso no se pintan igual: uno cuesta dinero y se presupuesta, el otro corta el servicio. La celda dice "se corta" en vez de una cifra y el resumen lleva un contador de cuotas rotas aparte del total, porque un total que suma cero cuando significa "esto no funciona" es peor que no tener total.
- ✓El plan recomendado es el más barato que NO choca contra un tope duro, no el más barato a secas: un gratis que se queda corto no cuesta cero, cuesta una caída. Cubierto en tests/infra-stack.test.ts.
- ✓Las tarifas de excedente de Vercel se IMPORTAN de src/lib/computo/tarifas.ts, las mismas con las que se le factura el cómputo a un cliente. Con aritmética propia cotizaría con un número y facturaría con otro, y la diferencia solo se vería al reclamar una factura.
- ✓Google Workspace no factura por consumo sino por asiento, y entra como cuota incluida cero con precio por unidad en vez de con una rama especial en el cálculo: una excepción escrita para un solo proveedor es la que nadie vuelve a probar.
- ✓EL PRO DE VERCEL NO TIENE CUOTA POR RECURSO, tiene 20 dólares de crédito de uso, y modelarlo como cuotas (la primera versión) regalaba un terabyte de transferencia inexistente. El crédito se resta del consumo total y no se reparte entre dimensiones, para que la tabla siga enseñando lo que cuesta cada una y el descuento se vea como lo que es. Turso llegó con tres errores más y dos planes de menos; todo salió de abrir las páginas de precios, no de recordarlas.
- ✓Cuotas y precios llevan fecha de verificación VISIBLE en cada tarjeta y enlace a la página de tarifas. Los tres proveedores cambian condiciones sin avisar y un simulador desactualizado no falla con un error: miente con confianza.
Como operador, quiero mover el uso a mano y ver el efecto al instante, sin recargar ni esperar al servidor
- ✓El módulo es PURO E ISOMORFO: el servidor pinta el primer escenario y el navegador recalcula importando el mismo archivo, sin BD ni node:crypto de por medio. Misma decisión que en computo/calculo.ts y por el mismo motivo.
- ✓El uso sale SIEMPRE de los inputs, que son la única fuente de verdad de la vista: editar una celda y cambiar de escenario después no pueden discrepar.
- ✓25 casos en tests/infra-stack.test.ts, incluida una comprobación estructural de que ningún plan deja una dimensión sin declarar cuota ni excedente: ese hueco se calcularía como gratis ilimitado, que es el error silencioso más caro de la página.
- ✓Un test vigila que la selección inicial siga siendo la tarifa más baja de cada proveedor: si entra un plan más barato y nadie mueve el default, salta.
Como operador, quiero que el simulador refleje las tarifas vigentes de Turso y no las de cuando lo escribí
- ✓LA FECHA DE VERIFICACIÓN CUMPLIÓ SU FUNCIÓN a los cinco días: la página de precios ya no decía lo mismo. La cuota gratuita de lecturas bajó de mil millones a 500 millones, apareció la dimensión de sincronización y entraron los planes Developer (el primero que cobra excedente en vez de cortar) y Pro.
- ✓Se carga la columna de facturación ANUAL y la fuente de la tarjeta dice cuánto cuesta mes a mes, para que nadie compare el simulador con la factura y crea que está mal.
- ✓Las lecturas pasan a miles de millones por mes, que es la unidad en la que Turso factura. En millones, el precio unitario de Developer y el de Scaler se redondeaban al mismo valor en pantalla siendo distintos; el formato ahora da cuatro decimales por debajo del centavo.
- ✓tests/infra-stack.test.ts y e2e/infra.spec.ts ajustados a los nuevos topes y precios, con los tres escenarios reescritos en la unidad nueva.
Como operador, quiero que sea imposible que una prueba de carga acabe pegándole a producción
- ✓TRES CAPAS PARA LA MISMA REGLA, cada una donde la anterior ya no alcanza: el script (URL y base del objetivo), el workflow (antes de gastar un minuto de runner) y la ingesta (si alguien corre k6 a mano saltándose el script, el resultado no entra al panel).
- ✓La lista de dominios prohibidos está DUPLICADA A PROPÓSITO porque perfil.js corre en el runtime de k6 y no puede importar TypeScript. Lo que la mantiene honesta es un test que lee el .js y compara ambas copias: relajar una sin la otra deja un agujero silencioso.
- ✓EL BLOQUEO NO ERA VERCEL_TOKEN. El plan llevaba desde julio diciendo que la fase esperaba esa credencial para tener un preview donde correr; el guardarraíl de agosto exige que el objetivo lea de una base LOCAL y un preview lee Turso, así que la corrida abortaría en setup() con el secret o sin él. El workflow levanta el sitio en el propio runner contra bases libsql desechables, las mismas de los e2e.
- ✓Se dice en el propio workflow qué mide y qué no: un runner de 2 vCPU caracteriza la aplicación, no la infraestructura de Vercel, y los números absolutos solo son comparables contra otras corridas del mismo tipo.
- ·Disparar load-test.yml una vez desde Actions: los scripts y la ingesta se probaron de punta a punta en local, pero los tiempos de arranque del servidor efímero en el runner solo los da una corrida real.
Como operador, quiero saber cuánto de la cuota gratis se come cada proyecto, porque pasarse no cobra: apaga la cuenta entera
- ✓LA PREMISA SE VERIFICÓ ANTES DE CONSTRUIR: el plan de la cuenta con la API de Vercel (hobby), la API de costos con la CLI (404 en Hobby) y el modelo de cobro de Fluid en la documentación. Cobrar "costo más margen" en Hobby da siempre cero, así que el sistema pasó de facturar a vigilar.
- ✓Las cuotas se importan del catálogo VERCEL de infra-stack.ts, el mismo que simula /admin/infra: una sola lista de límites para las dos páginas.
- ✓La proyección usa el ritmo OBSERVADO y no el mes entero: con el medidor instalado el día 20, dividir por 20 días diluiría el ritmo a menos de la mitad.
- ✓Avisos por ntfy al 70, 90 y 100 % y por proyección, solo el umbral más alto recién cruzado y una vez por mes, con el estado en app_settings como el detector de crons en silencio. Diario y no por sondeo: una cuota mensual no se agota en horas salvo un ataque, y eso ya lo ve el micro-SIEM.
- ·Medir también el propio portafolio: mientras no tenga medidor, el porcentaje de la cuota excluye a codebymike.net.
Como operador, quiero medir el cómputo de un sitio de cliente sin exigirle dependencias ni arriesgar sus peticiones
- ✓UN SOLO ARCHIVO SIN IMPORTS (instrumentacion/medidor.ts) que se copia al proyecto, en TypeScript de sintaxis borrable: Node 24 lo cargó sin compilar en la verificación en vivo.
- ✓LA MEMORIA SE MIDE COMO LA COBRA VERCEL: por vida de la instancia mientras tiene peticiones en curso. Dos peticiones solapadas pagan el tiempo una vez, y la CPU se lee del proceso entero por la misma razón.
- ✓La firma y el formato del lote están escritos dos veces (el medidor no puede importar nada del portafolio): lo que los mantiene iguales es una prueba que pasa lo que envía el medidor por verificarLote y validarLote.
- ✓Punto ciego declarado en el panel y en el plan: lo que el CDN sirve sin despertar la función. Transferencia al visitante y peticiones al edge se pintan como cota inferior.
- ✓INSTALADO EN TRES REPOS DE CLIENTE (23 sep): Toledo (Astro en modo servidor, en secuencia con su login), DobleYo (páginas Astro de servidor y API Express 4 en Node 20) y Gorillaz (Express 5 en CommonJS). Para los dos últimos hacía falta JavaScript: medidor.mjs y medidor.cjs se GENERAN desde el .ts, y una prueba falla si quedan desalineados. Prueba de humo con el Express real de cada repo: respuestas intactas y bytes exactos.
- ✓Revisar esos repos destapó dos fallos del medidor antes de que llegaran a producción: Astro corre el middleware también al prerenderizar en el build, y esos renders se habrían contado como visitas; y el listener de SIGTERM se enganchaba al importar el módulo, así que el proceso del build también quedaba escuchando la señal.
- ·Poner COMPUTO_PROYECTO y COMPUTO_SECRETO en los tres proyectos de Vercel y contrastar una semana contra el tablero Usage (Fase 5 del plan). Queda sin medir, a sabiendas, el middleware de enrutamiento de DobleYo.
Como operador, quiero que un reintento o una caída de la base nunca cuenten dos veces ni pierdan un lote
- ✓LA INGESTA DEL 7 SEP PERDÍA LOTES: marcaba el lote y después sumaba las horas en pasos sueltos, y tomaba CUALQUIER error de la marca como "duplicado". Una caída entre los dos pasos dejaba la marca sin su consumo y el reintento se daba por entregado.
- ✓Marca y horas van ahora en un solo db.batch, que libSQL aplica como transacción, y solo un UNIQUE se lee como duplicado (isUniqueViolation); cualquier otro fallo responde 503 para que el medidor reintente.
- ✓La prueba de punta a punta renombra la tabla horaria a mitad del lote, y se comprobó que falla contra la versión anterior de la ingesta y pasa con la nueva.
Como operador, quiero cotizar el hosting de un cliente nuevo con un número defendible aunque el cómputo me cueste cero
- ✓El precio sugerido cubre el día en que el uso comercial obligue a pasar a Pro: el mayor entre su cómputo a tarifa Pro con margen y su parte del asiento Pro. Para un sitio pequeño el cómputo son centavos y manda el asiento, y la página lo dice.
- ✓Estima desde visitas y no desde invocaciones: un sitio estático tiene cero invocaciones y aun así gasta la transferencia, que es la cuota que primero se agota en un sitio con imágenes.
- ✓Módulo puro e isomorfo con la misma calcularCobro que habría facturado el modelo anterior; e2e/computo.spec.ts prueba el recálculo en el navegador por el pase de demo.
Como operador, quiero que /automatizaciones haga visible el silencio de un cron
- ✓Partitura de 24 h con una marca por ejecución real y cada silencio mayor a lo tolerado teñido con su duración (RF-029).
- ✓Simulacro del corte del 7 sep sobre los datos de hoy: el aviso llega cuando llegaría el detector real, con el texto real del push.
- ✓BUG CORREGIDO: siete de once tareas salían "sin registro" porque la página leía las últimas 150 filas (unas 9 h); ahora lee un rango de 48 h.
- ✓tests/motion-automatizaciones.test.ts (13 casos).
Como operador, quiero que la temporada se encienda y se apague sola sin tocar el código
- ✓src/lib/temporada.ts, puro: decide por el mes en hora de Bogotá; el 1 de noviembre los fantasmas desaparecen sin desplegar.
- ✓tests/temporada.test.ts (3 casos), con la frontera en hora de Bogotá y no en UTC.
Como ponente (3)
Como ponente, quiero entrar a la demo del portal y navegar el panel delante del jurado sin salirme de la presentación
- ✓LA INTERACCIÓN VA POR DENTRO, no por fuera. Quitarle el pointer-events:none al iframe devolvía un fallo entero: el bundle escucha onStageClick y cualquier clic en el escenario avanza un beat. Se afina escribiendo estilo DENTRO del documento del mazo (todo es mismo origen), con el body inerte y solo el iframe vivo receptivo, y se re-afirma en cada sondeo porque el bundle monta y desmonta esos iframes al cambiar de beat.
- ✓El teclado se le tapa al bundle con un listener en fase de CAPTURA, el reverso exacto del disparo sintético de la reconciliación: aquel inyecta el evento con el constructor del iframe, este lo intercepta antes de que el bundle lo vea, y el discriminador es isTrusted. Sin eso una flecha con el foco dentro del mazo movería un beat SIN pasar por el servidor y la sala se quedaría atrás.
- ✓El precio se documenta antes de estar delante del tribunal: con el foco dentro de la demo las flechas no pasan de diapositiva, y se sale clicando fuera o se usa el móvil.
- ✓El código que descubre la forma del mazo vive una sola vez en src/lib/presentacion/lienzo.ts porque lo necesitan las dos ventanas: 38 casos. Dos copias divergiendo es el fallo que nadie nota hasta que una se queda en la diapositiva de al lado.
- ✓Ruta sin puerta a propósito y con guión, no con barra: /present-admin queda fuera de startsWith("/present/") y de isAdmin POR CONSTRUCCIÓN, sin excepciones que añadir al middleware. Una sesión de GitHub caducada el día de la charla dejaría sin mando, y lo que se enmarca ya es público.
Como ponente, quiero recorrer la página viva de un beat sin que tres toques seguidos se pierdan
- ✓EL SCROLL ES OTRA POSICIÓN ABSOLUTA, no un comando relativo: misma decisión que sostiene el resto del sistema. Un sondeo perdido no pierde nada, tres toques valen tres porque se acumulan sobre lo PEDIDO y no sobre lo que se ve (que va por detrás mientras la animación corre), y el estado entero cabe en un número.
- ✓El número viaja con la diapositiva a la que pertenece: sin ese sello, el scroll del portal se seguiría aplicando tras pasar a /status. Acotado contra la geometría publicada y descarte de un y imposible en src/lib/presentacion/desplazamiento.ts, 24 casos.
- ✓Los dos botones ↑/↓ ocupan el sitio de la rejilla de saltos solo mientras la diapositiva tenga algo que desplazar, con repetición al mantener pulsado.
- ✓La rueda del ratón escribe en la MISMA clave que el pulgar y se acepta que sean dos escritores: esta ventana es un mando, la ventana de carrera es de milisegundos y el peor caso es un salto de scroll.
- ✓El acotado de dos escrituras por segundo vive en el CLIENTE, no en el servidor: un servidor que descarta en silencio lo que le mandan deja la sala en una posición que nadie puede explicar mirando el estado.
Como ponente, quiero saber cuánto llevo sin que la sala vea correr mi reloj
- ✓El cronómetro vive solo en /present-admin. En la pared no aparece: ver correr el reloj de quien está sustentando cambia cómo se le escucha.
- ✓EL ARRANQUE ES DEL SERVIDOR, no del navegador, y arranca SOLO con el primer movimiento que saca la presentación de su primera diapositiva. No hay botón de empezar: es un gesto más que recordar con la sala esperando, y el que se olvida. Una recarga a mitad de charla está contemplada y no pone el reloj a cero.
- ✓La cuenta corrige el desfase entre los dos relojes con el ahora que ya viaja en el sondeo, sin una petición extra. Arranque idempotente y formato en src/lib/presentacion/cronometro.ts, 20 casos.
- ✓La isla flotante lleva pointer-events:none en el contenedor y auto solo en el pastillero: si se comiera los clics de la franja superior se llevaría por delante justo la barra de navegación del portal durante la demo.
Como visitante del /lab (1)
Como visitante del /lab, quiero ver hasta dónde aguanta el sistema sin que eso sea un mapa para atacarlo
- ✓La sección pública publica comportamiento (sostenido, quiebre, recuperación, p95) y NUNCA el objetivo: decir contra qué entorno se corre es decir dónde hay algo menos vigilado que producción.
- ✓La tarjeta "en construcción" de k6 desaparece sola cuando hay una corrida ingerida, con el mismo mecanismo que ya usaban SAST y mutation: el estado de la página lo decide el dato, no una bandera escrita a mano.
- ✓Una sola fila leída y no una agregación: la tabla crece una fila por corrida manual, así que la vitrina no repite el problema de coste que dejó escanear monitor_checks en una ruta pública.
- ✓Texto en los dos diccionarios (es/en), verificado por el test de paridad.
Como lector (3)
Como lector, quiero ver en /notes la decisión de cada artículo antes de leerlo
- ✓Ficha que recorre las decisiones reales (problema, opción descartada, opción elegida) sobre el mapa generativo de cada nota (RF-027).
- ✓Bitácora por meses con filtros por familia en la URL, índice lateral con minutos restantes y tres notas relacionadas.
- ✓Cada nota publicada trae su campo decision en los dos idiomas; tests/motion-notas.test.ts (21 casos) lo exige.
Como lector, quiero entender cómo se cotiza consultoría sin que el alcance se descontrole
- ✓Nota /notes/el-precio-pactado-no-se-toca en español e inglés, sin tarifas, rutas del panel ni umbrales de los frenos.
Como lector, quiero entender cómo se le da permiso de escribir a un asistente sin perder el control
- ✓Nota /notes/un-asistente-que-escribe-con-permiso en español e inglés, sin rutas del panel ni detalles de las defensas.
Como Mike (12)
Como Mike, quiero armar una propuesta en la que el precio se ajuste a medida que la configuro
- ✓Constructor en /admin/plano con el mismo motor de precios del sitio; las preguntas cierran el rango sin salir de la tabla (RF-214).
- ✓Reglas de pago por monto, cuotas con recargo comparado con la usura y fechas hábiles con quincenas y ciclos de empresa, editables en /admin/plano/ajustes (RF-215).
- ✓Versiones congeladas con huella SHA-256, diferencias entre versiones y PDF.
- ✓Recorrido completo con capturas reales del constructor contra una base local el 6 oct 2026.
- ✓Migraciones 0041 a 0043 aplicadas en Turso, principal y demo.
- ·Aceptación de Mike con una propuesta real.
Como Mike, quiero que la IA arme el borrador desde la conversación y revise la propuesta como un cliente difícil
- ✓Salida estructurada sin campos de dinero; solo componentes de la tabla; citas verificadas como literales (RF-216).
- ✓Hallazgos con cifras inventadas descartados por la guardia de cifras del asistente.
- ✓Probado con el modelo real el 6 oct 2026: conversación de una panadería a 6 componentes con sus 4 citas verificadas, exclusiones y preguntas útiles (US$0,06).
Como Mike, quiero que el anticipo pagado se convierta solo en proyecto
- ✓Cliente, proyecto, hitos, cuentas de cobro en borrador e invitación al portal, una sola vez aunque el webhook se repita (RF-218).
- ✓tests/plano-db.test.ts recorre el flujo completo contra libSQL temporal con las migraciones reales.
Como Mike, quiero preguntarle al panel en lenguaje normal y encontrar cualquier cosa sin navegar el menú
- ✓Caja al estilo de Notion con búsqueda instantánea sin costo y Ctrl+K en todo el panel (RF-219).
- ✓Consultas nuevas del asistente: pagos recibidos, vencimientos y búsqueda en el panel, sin datos personales (RF-210).
- ✓Respuesta con pasos visibles y solo enlaces internos; conversaciones recientes que se pueden retomar.
- ✓Capturas en escritorio y móvil con GPU real.
- ✓Probado con el modelo real contra la base demo: pagos, deudas y dominios con sus enlaces (unos US$0,03).
Como Mike, quiero pedirle una cuenta de cobro y que haga todo el proceso hasta el borrador
- ✓Busca cliente y proyecto, pregunta lo que falte y propone; la tarjeta muestra líneas, retenciones, neto y lo que falta para emitir.
- ✓Nada se escribe sin "Aprobar"; "Pedir cambios" vuelve al modelo como indicación; la propuesta sobrevive a recargar.
- ✓Borrador creado con createCuentaCobro y auditado; emitirla sigue siendo un clic de Mike.
- ✓e2e/asistente.spec.ts recorre aprobar y descartar con la API de Claude falsa.
- ✓Migración 0044 aplicada en las dos bases; cuenta de cobro real creada y aprobada en la demo (US$0,032).
Como Mike, quiero entrar a Cotiza con un PIN sin que ese PIN abra el resto del panel
- ✓Rutas abiertas por el PIN con patrones anclados; cualquier otra de /admin va a /login (RF-220).
- ✓Frenos por IP y general, falla cerrada, aviso por ntfy en cada entrada y PIN triviales rechazados.
- ✓tests/cotiza-acceso.test.ts y tests/cotiza-pin-db.test.ts (30 casos) y recorrido real de 34 comprobaciones.
- ·Fijar el PIN en /admin/settings en producción.
Como Mike, quiero cotizar por entregables contables con un precio que se congela
- ✓Motor puro e isomorfo: horas por entregable, colchón, redondeo, tramos de pago de Plano, cupos y adicionales (RF-221).
- ✓Al congelar, el servidor recalcula y guarda tarifas, cupos y reglas del día con huella SHA-256.
- ✓tests/cotiza-motor.test.ts (29 casos con cifras a mano), en UTC, Bogotá y Tokio.
- ·Confirmar el reparto 60/70/80, las horas de revisión de documentos y el mínimo por encargo.
Como Mike, quiero que la bitácora decida sola cuándo algo ya no cabe en lo pactado
- ✓Pedidos, reuniones y rondas anotados en una transacción con su adicional; recargo por la hora del pedido en Bogotá (RF-222).
- ✓Textos para WhatsApp de la propuesta, de cada adicional y de cada resumen de reunión.
- ✓Dictado por voz en los campos largos con el reconocimiento del navegador; micrófono permitido solo en las páginas de Cotiza (tests/cotiza-dictado.test.ts, 12 casos).
- ✓tests/cotiza-encargos.test.ts (20 casos con el SQL real de las migraciones) y recorrido en el navegador con capturas.
- ✓Migraciones 0045 y 0046 aplicadas en Turso, principal y demo, el 6 oct 2026.
Como Mike, quiero que la IA convierta el pedido en alcance, clasifique pedidos nuevos y ordene mis notas
- ✓Esquemas sin campos de dinero, citas literales verificadas y guardia que quita cifras que no venían en el texto (RF-223).
- ✓tests/cotiza-ia.test.ts (14 casos) y recorrido contra una API falsa con citas y cifras inventadas a propósito.
- ✓Probado con el modelo real el 6 oct 2026: 4 entregables con citas literales, un pedido de noche clasificado como adicional citando la exclusión y un resumen limpio (US$0,11 las tres consultas). Corregido el prompt para que los supuestos no contradigan los cupos ni se dirijan a Mike.
Como Mike, quiero pedirle al asistente que cambie un proyecto, un hito, el seguimiento o los mensajes, y aprobarlo viendo el cambio
- ✓Cuatro escrituras con tarjeta común de antes y después (RF-210): proyecto, hito, seguimiento y mensajes leídos.
- ✓Lo público del portafolio y la visibilidad de un hito no se tocan aunque el modelo los mande; las notas internas solo crecen.
- ✓La regla de hitos que avisa al cliente pasó a un módulo compartido con el panel; la tarjeta lo dice antes de aprobar.
- ✓tests/asistente-escrituras.test.ts (19 casos) y e2e de un mensaje marcado como leído con la API de Claude falsa.
Como Mike, quiero pegarle lo que me escribió un cliente y encontrar la propuesta lista en Plano
- ✓crear_propuesta deja el borrador en Plano y lo lee con su IA al aprobar, no al preparar (RF-211).
- ✓Devuelve el rango del motor y las preguntas abiertas, sin el teléfono ni el correo que haya en la conversación.
- ✓tests/asistente-propuesta.test.ts (4 casos) con la IA de Plano falsa, incluido el fallo de la IA.
Como Mike, quiero saber qué hace el modelo real con preguntas reales antes de confiarle escrituras
- ✓scripts/asistente-banco-casos.ts: 18 casos contra la base demo que se califican solos y dejan la demo como estaba.
- ✓Primera corrida: 14 de 18; corregidos la guardia de dólares con centavos, la capacitación enviada a Plano y la búsqueda de un hito por nombre.
- ✓Los casos afectados, repetidos: 6 de 6. Las tres trampas de terceros se denunciaron sin obedecerlas. Unos US$0,90 entre las tres corridas.