{ "feature_id": "TPV-DEV-IN-PROD", "verdict": "APPROVED", "reviewer": "reviewer", "reviewed_at": "2026-08-27T21:13:00Z", "scope": [ "project/scripts/monolith.sh", "docs/HOWTO-monolith.md", "docs/pos/POS_OPERATIONS.md" ], "summary": "Smoke test post-arranque añadido a monolith.sh detecta con tres dimensiones independientes (HTML markers, endpoint /_next/hmr, banner (Turbopack)) si un servicio Next está sirviendo `next dev` en lugar de `next start`. Cobertura transversal a los 4 servicios Next (admin, tpv, frontend, storefront). Documentación endurecida con sección §3.1 de errores comunes + runbook de recuperación en HOWTO-monolith, y nota inline en POS_OPERATIONS §1.3. Acepto el fix.", "checks": [ { "id": "syntax", "description": "Bash syntax valid", "command": "bash -n project/scripts/monolith.sh", "result": "PASS", "evidence": "Exit code 0. Sin errores." }, { "id": "function-extractable", "description": "Función smoke_check_no_dev_markers definida y extraíble", "command": "awk '/^smoke_check_no_dev_markers\\(\\)/,/^}/' project/scripts/monolith.sh", "result": "PASS", "evidence": "Bloque de 53 líneas extraído correctamente; firma `smoke_check_no_dev_markers() { … return 0; }`." }, { "id": "mock-dev-fails", "description": "Mock con HTML HMR marker + 200 en /_next/hmr → smoke debe fallar", "command": "python3 mock en :30999 + smoke_check_no_dev_markers", "result": "PASS", "evidence": "Salida: '[FAIL] mock root HTML contains dev-only marker: /__next_hmr' + '[FAIL] mock /_next/hmr returned HTTP 200 (expected 404 or 426)' + 'exit=1'." }, { "id": "mock-prod-ok", "description": "Mock con HTML limpio + 404 en /_next/hmr → smoke debe pasar", "command": "python3 mock en :30994 + smoke_check_no_dev_markers", "result": "PASS", "evidence": "Salida: '[OK] All Next.js services are HMR-free (prod start confirmed). exit=0'." }, { "id": "docs-coverage", "description": "Documentación cubre señales, runbook y checklist", "command": "grep -nE '^### 3\\.1|^## 6\\.|3\\.1 Errores comunes' docs/HOWTO-monolith.md", "result": "PASS", "evidence": "§3.1 añadida con 4 sub-bloques (por qué, señales, runbook, post-fail). §6 incluye nuevo bloque 'Comprobaciones adicionales anti-dev'. POS_OPERATIONS §1.3 incluye bloque de advertencia." }, { "id": "scope-discipline", "description": "No se modifica nada fuera del scope acordado", "command": "git diff --stat project/scripts/monolith.sh docs/", "result": "PASS", "evidence": "Solo 3 archivos: project/scripts/monolith.sh (+79), docs/HOWTO-monolith.md (+63/-1), docs/pos/POS_OPERATIONS.md (+2/-0). Cero cambios en next.config.ts, package.json, proxy.ts, ni archivos fuera del scope." }, { "id": "set-e-safety", "description": "Interacción correcta con set -euo pipefail", "command": "code review", "result": "PASS", "evidence": "Llamada envuelta en `if ! smoke_check_no_dev_markers; then … exit 1; fi`. El operador `!` neutraliza errexit. `stop_all` retorna 0 en todas las ramas (stop_pid devuelve 0 incluso con pid vacío). `exit 1` final se ejecuta siempre si la condición se cumple." }, { "id": "redirect-chain-handling", "description": "Comportamiento correcto ante redirect / → /login del proxy.ts", "command": "code review", "result": "PASS", "evidence": "TPV proxy.ts redirige `/` no autenticado a `/login`. La página /login es server-rendered Next prod sin markers HMR. Curl con `-fsS` sigue el redirect y obtiene HTML de /login. grep -qF sobre ese HTML no encuentra markers → pasa. Si un dev server estuviera activo, inyectaría HMR en /login también → el smoke lo detectaría vía redirect chain." }, { "id": "performance", "description": "Overhead aceptable del smoke", "command": "estimación + 2 mocks ejecutados", "result": "PASS", "evidence": "4 servicios × (curl --max-time 5 + grep) ≈ 20s worst case. En estado saludable (<100ms por curl) el smoke termina en <500ms. Despreciable vs build." }, { "id": "no-new-deps", "description": "Cero dependencias nuevas", "command": "grep -nE 'apt|yarn add|npm i|brew install' project/scripts/monolith.sh docs/", "result": "PASS", "evidence": "Solo bash builtins (printf, grep, head, local), curl (ya usado por wait_http). Sin nuevas dependencias." } ], "findings": [], "nits": [ { "id": "nit-1", "severity": "info", "description": "El smoke test cubre 4 servicios Next secuencialmente. Para deploys con muchos servicios podría paralelizarse con `&` + `wait`. Aceptable hoy porque el coste es ~500ms; revisar si se añaden más apps Next.", "suggested_action": "Monitorizar. No bloquea el fix." }, { "id": "nit-2", "severity": "info", "description": "`local service port url log body hmr_code marker failed=0` mezcla declaración sin valor y asignación en la misma línea. Sintaxis válida; estilo alternativo es declarar primero y asignar después.", "suggested_action": "Estilo OK, no requiere cambio." }, { "id": "nit-3", "severity": "info", "description": "El bloque §3.1 de HOWTO-monolith menciona 'WebSocket /_next/hmr' como señal observable. Un operador sin acceso al DevTools del navegador podría no saber dónde mirar.", "suggested_action": "Añadir en una iteración futura un comando `curl -i https://tpv-mv.rikrdo.com/_next/hmr` con la salida esperada como referencia. No bloquea." } ], "verdict_rationale": "El fix cumple el objetivo del ticket con tres capas de defensa (smoke test en 3 dimensiones, documentación operativa, runbook de recuperación). Verificación funcional contra mocks controlados confirma que el smoke detecta dev servers y deja pasar prod servers. La sintaxis bash es válida; la interacción con `set -e` es correcta. Scope respetado: cero cambios fuera de los 3 archivos acordados. Verdict: APPROVED.", "evidence_paths": [ "work/artifacts/TPV-DEV-IN-PROD/architect.md", "work/artifacts/TPV-DEV-IN-PROD/implementer.md", "project/scripts/monolith.sh (líneas 368-435)", "docs/HOWTO-monolith.md (§3.1 + §6)", "docs/pos/POS_OPERATIONS.md (§1.3)" ] }