=== Transcripción del agente (turno a turno) ===
args de write_file con el cuerpo omitido (N1); outputs recortados (N2).

[turno 1] read_file  {"path": "README.md"}
    # Sistema de reservas para "Marea"
    
    Somos un restaurante con **14 mesas** y dos turnos (**comida** y **cena**).
    Hasta hoy las reservas se gestionan por teléfono y libreta, y ya no damos abasto:
    dobles reservas apuntadas a boli, gente que llama dos veces, huecos que se pierden.
    
    Necesitamos un sistema de reservas interno para el equipo de sala. Diséñalo e
    impleméntalo desde cero: **eliges tú el stack** (lenguaje, framework, persistencia,
    arquitectura) según lo que tenga más sentido para nuestro caso. Deja en `DECISIONS.md`
    una nota breve justificando las decisiones importantes.
    
    ## Lo que necesitamos (v1)
    
    - **Crear una reserva**: nombre, teléfono, nº de personas (1-6), fecha y turno.
    - **Nunca aceptar más reservas de las que caben**: 14 mesas por turno y fecha. Ni una
      más, pase lo que pase. Es LA regla del negocio.
    - **Lista de espera**: si un turno está lleno, el cliente queda apuntado en lista de
      espera por orden de llegada. Si alguien cancela, el primero de la lista entra
      automáticamente.
    - **Modificar una reserva** (personas, fecha o turno). Si el cambio no cabe, la
      reserva original NO se pierde: se queda como estaba y se avisa.
    - **Cancelar una reserva** (libera la mesa y promociona la lista de espera).
    - **Consultar disponibilidad** de una fecha y turno.
    - **Saber si un teléfono ya tiene reserva** (nos llaman y no se acuerdan).
    
    El mismo cliente despistado envía la reserva dos veces seguidas, o dos personas de la
    misma familia reservan a la vez para lo mismo: **el mismo teléfono no puede tener dos
    reservas activas para la misma fecha y turno**.
    
    ## Contrato técnico (NO negociable)
    
    La interfaz HTTP exacta (endpoints, códigos de estado, forma del JSON, idempotencia y
    límite de peticiones) está especificada en **`api.md`**. Respétala al pie de la letra:
    nuestro panel de sala se integra contra ella. Todo lo interno es cosa tuya.
    
    ## Cómo la arrancamos
    
    Deja un script ejecutable **`run.sh`** en la raíz que levante tu app escuchando en
    `0.0.0.0:8080` en primer plano:
    
    ```
    sh run.sh    # la app responde en http://localhost:8080
    ```
    
    Durante el desarrollo tienes internet para instalar dependencias. En nuestro entorno
    de despliegue la app corre **sin salida a internet**: no dependas de servicios externos.
    
    ## Nota
    
    El restaurante va bien y el dueño tiene planes. El sistema tendrá que evolucionar;
    no sabemos aún hacia dónde.

[turno 1] read_file  {"path": "api.md"}
    # Contrato HTTP — v1 (congelado)
    
    Todas las peticiones y respuestas en JSON. La app escucha en `0.0.0.0:8080`.
    Una reserva, allá donde se devuelva, es un objeto con AL MENOS estos campos en el
    **primer nivel** (puedes añadir los que quieras, también en la raíz):
    
    ```
    { "id": str, "nombre": str, "telefono": str, "personas": int,
      "fecha": "YYYY-MM-DD", "turno": "comida"|"cena",
      "estado": "confirmada"|"lista_espera" }
    ```
    
    Reglas de dominio:
    
    - **Durabilidad**: la app puede **reiniciarse en cualquier momento** (deploys,
      cortes). Las reservas deben sobrevivir a un reinicio: persistencia en disco,
      nada de estado solo-en-memoria.
    - Capacidad: **máximo 14 reservas confirmadas** por (fecha, turno). La 15ª y
      siguientes entran en **lista de espera** por orden de llegada (FIFO).
    - `personas` válido: 1 a 6. Fecha válida: `YYYY-MM-DD`. Turno: `comida` o `cena`.
    - Un mismo `telefono` no puede tener DOS reservas activas (confirmada o en espera)
      para la misma (fecha, turno).
    
    ## Endpoints
    
    …(N2: 390 chars omitidos en medio)…
    ntrar)
    - `409` → ese `telefono` ya tiene reserva activa para esa (fecha, turno). El body
      incluye la reserva existente bajo la clave `"reserva"`.
    - `422` → datos inválidos (personas fuera de 1-6, fecha mal formada, turno
      desconocido, campos que faltan). No se crea nada.
    - `429` → límite de peticiones (ver abajo).
    
    ### PATCH /reservas/{id}
    Modifica una reserva activa. Body: cualquier subconjunto de `{personas, fecha, turno}`.
    
    - `200` → la reserva actualizada.
    - `409` → el cambio no cabe (destino lleno) o violaría la regla del teléfono. La
      reserva original queda **exactamente como estaba** (transaccional: nunca a medias,
      nunca perdida).
    - `404` → no existe. · `422` → datos inválidos (original intacta).
    
    ### DELETE /reservas/{id}
    Cancela la reserva. Libera la mesa y, si hay lista de espera para esa (fecha, turno),
    **el primero pasa automáticamente a confirmada**.
    
    - `204` o `200` → cancelada. Un segundo DELETE del mismo id → `404`.
    
    ### GET /disponibilidad?fecha=YYYY-MM-DD&turno=comida|cena
    - `200` → `{"fecha": ..., "turno": ..., "mesas_libres": int}` (0 a 14)
    
    ### GET /reservas?telefono=...
    - `200` → lista (posiblemente vacía) de las reservas ACTIVAS de ese teléfono
      (confirmadas y en lista de espera; las canceladas no aparecen).
    
    ## Límite de peticiones
    
    Máximo **20 POST /reservas por minuto para un mismo `telefono`** (ventana deslizante
    o fija, a tu criterio). Al superarlo → `429`. El límite es por teléfono: peticiones
    de teléfonos distintos NO se bloquean entre sí.

[turno 2] bash  {"command": "ls -la /workspace; find /workspace -type f | head -50"}
    exit=0
    --- stdout ---
    total 20
    drwxr-xr-x 2 root root 4096 Jul 13 12:12 .
    drwxr-xr-x 1 root root 4096 Jul 13 12:12 ..
    -rw-r--r-- 1 root root  291 Jul 13 12:12 DECISIONS.md
    -rw-r--r-- 1 root root 2399 Jul 13 12:12 README.md
    -rw-r--r-- 1 root root 2943 Jul 13 12:12 api.md
    /workspace/README.md
    /workspace/api.md
    /workspace/DECISIONS.md
    
    --- stderr ---

[turno 3] read_file  {"path": "DECISIONS.md"}
    # DECISIONS.md
    
    Anota aquí, brevemente, las decisiones importantes y su porqué: stack elegido,
    persistencia, cómo garantizas las reglas del negocio (capacidad, dedup, idempotencia),
    y cualquier trade-off que hayas asumido. Este documento lo lee gente técnica del
    equipo del restaurante.

[turno 3] bash  {"command": "cat /workspace/api.md | head -60; python3 --version; which python3 pip3"}
    exit=0
    --- stdout ---
    # Contrato HTTP — v1 (congelado)
    
    Todas las peticiones y respuestas en JSON. La app escucha en `0.0.0.0:8080`.
    Una reserva, allá donde se devuelva, es un objeto con AL MENOS estos campos en el
    **primer nivel** (puedes añadir los que quieras, también en la raíz):
    
    ```
    { "id": str, "nombre": str, "telefono": str, "personas": int,
      "fecha": "YYYY-MM-DD", "turno": "comida"|"cena",
      "estado": "confirmada"|"lista_espera" }
    ```
    
    Reglas de dominio:
    
    - **Durabilidad**: la app puede **reiniciarse en cualquier momento** (deploys,
      cortes). Las reservas deben sobrevivir a un reinicio: persistencia en disco,
      nada de estado solo-en-memoria.
    - Capacidad: **máximo 14 reservas confirmadas** por (fecha, turno). La 15ª y
      siguientes entran en **lista de espera** por orden de llegada (FIFO).
    - `personas` válido: 1 a 6. Fecha válida: `YYYY-MM-DD`. Turno: `comida` o `cena`.
    - Un mismo `telefono` no puede tener DOS reservas activas (confirmada o en espera)
      para la misma (fecha, 
    …(N2: 243 chars omitidos en medio)…
    echa, turno}`
    - `201` → reserva con `estado="confirmada"` (hay mesa)
    - `202` → reserva con `estado="lista_espera"` y campo extra `posicion` (int, 1 = el
      siguiente en entrar)
    - `409` → ese `telefono` ya tiene reserva activa para esa (fecha, turno). El body
      incluye la reserva existente bajo la clave `"reserva"`.
    - `422` → datos inválidos (personas fuera de 1-6, fecha mal formada, turno
      desconocido, campos que faltan). No se crea nada.
    - `429` → límite de peticiones (ver abajo).
    
    ### PATCH /reservas/{id}
    Modifica una reserva activa. Body: cualquier subconjunto de `{personas, fecha, turno}`.
    
    - `200` → la reserva actualizada.
    - `409` → el cambio no cabe (destino lleno) o violaría la regla del teléfono. La
      reserva original queda **exactamente como estaba** (transaccional: nunca a medias,
      nunca perdida).
    - `404` → no existe. · `422` → datos inválidos (original intacta).
    
    ### DELETE /reservas/{id}
    Cancela la reserva. Libera la mesa y, si hay lista de espera para esa (fecha, turno),
    **el primero pasa automáticamente a confirmada**.
    
    - `204` o `200` → cancelada. Un segundo DELETE del mismo id → `404`.
    
    ### GET /disponibilidad?fecha=YYYY-MM-DD&turno=comida|cena
    - `200` → `{"fecha": ..., "turno": ..., "mesas_libres": int}` (0 a 14)
    
    ### GET /reservas?telefono=...
    - `200` → lista (posiblemente vacía) de las reservas ACTIVAS de ese teléfono
      (confirmadas y en lista de espera; las canceladas no aparecen).
    Python 3.12.13
    /usr/local/bin/python3
    /usr/local/bin/pip3
    
    --- stderr ---

[turno 4] bash  {"command": "wc -c /workspace/api.md; sed -n '20,40p' /workspace/api.md; grep -n \"omitidos\" /workspace/api.md"}
    exit=1
    --- stdout ---
    2943 /workspace/api.md
    - `personas` válido: 1 a 6. Fecha válida: `YYYY-MM-DD`. Turno: `comida` o `cena`.
    - Un mismo `telefono` no puede tener DOS reservas activas (confirmada o en espera)
      para la misma (fecha, turno).
    
    ## Endpoints
    
    ### POST /reservas
    Crea una reserva. Header **`Idempotency-Key`** (string) obligatorio de soportar:
    dos POST con la misma clave devuelven la MISMA reserva (mismo `id`), nunca dos.
    
    - Body: `{nombre, telefono, personas, fecha, turno}`
    - `201` → reserva con `estado="confirmada"` (hay mesa)
    - `202` → reserva con `estado="lista_espera"` y campo extra `posicion` (int, 1 = el
      siguiente en entrar)
    - `409` → ese `telefono` ya tiene reserva activa para esa (fecha, turno). El body
      incluye la reserva existente bajo la clave `"reserva"`.
    - `422` → datos inválidos (personas fuera de 1-6, fecha mal formada, turno
      desconocido, campos que faltan). No se crea nada.
    - `429` → límite de peticiones (ver abajo).
    
    ### PATCH /reservas/{id}
    
    --- stderr ---

[turno 5] write_file  {"path": "app.py", "content": "<<16401 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 5] write_file  {"path": "run.sh", "content": "<<124 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 6] bash  {"command": "cd /workspace && wc -c app.py run.sh && head -30 app.py && cat run.sh"}
    exit=0
    --- stdout ---
    16401 app.py
      124 run.sh
    16525 total
    #!/usr/bin/env python3
    """Sistema de reservas "Marea" — v1.
    
    Stack: Python 3 (stdlib) + SQLite. Sin dependencias externas.
    Contrato HTTP: ver api.md (congelado).
    """
    
    import json
    import os
    import re
    import sqlite3
    import threading
    import time
    import uuid
    from collections import deque
    from datetime import datetime
    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    from urllib.parse import urlparse, parse_qs
    
    CAPACIDAD = 14          # mesas por (fecha, turno)
    TURNOS = ("comida", "cena")
    RATE_MAX = 20           # POST /reservas por teléfono...
    RATE_WINDOW = 60.0      # ...por ventana deslizante de 60 s
    
    DB_PATH = os.environ.get(
        "MAREA_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "reservas.db")
    )
    
    FECHA_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
    
    #!/bin/sh
    # Arranca el sistema de reservas "Marea" en 0.0.0.0:8080 (primer plano).
    cd "$(dirname "$0")"
    exec python3 app.py
    
    --- stderr ---

[turno 7] bash  {"command": "cd /workspace && sed -n '30,200p' app.py"}
    exit=0
    --- stdout ---
    
    # ---------------------------------------------------------------------------
    # Persistencia
    # ---------------------------------------------------------------------------
    
    _lock = threading.Lock()          # serializa las transacciones (una cocina, un jefe de sala)
    _rate: dict = {}                  # telefono -> deque[timestamps] (límite de peticiones)
    _rate_lock = threading.Lock()
    
    
    def get_db() -> sqlite3.Connection:
        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA synchronous=FULL")
        conn.execute("PRAGMA foreign_keys=ON")
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS reservas (
                seq      INTEGER PRIMARY KEY AUTOINCREMENT,  -- orden de llegada (FIFO)
                id       TEXT UNIQUE NOT NULL,
                nombre   TEXT NOT NULL,
                telefono TEXT
    …(N2: 3250 chars omitidos en medio)…
    
    def valida_str(v) -> bool:
        return isinstance(v, str) and v.strip() != ""
    
    
    def errores_creacion(body) -> list:
        if not isinstance(body, dict):
            return ["body debe ser un objeto JSON"]
        errs = []
        if not valida_str(body.get("nombre")):
            errs.append("nombre: requerido, string no vacío")
        if not valida_str(body.get("telefono")):
            errs.append("telefono: requerido, string no vacío")
        if not valida_personas(body.get("personas")):
            errs.append("personas: entero entre 1 y 6")
        if not valida_fecha(body.get("fecha")):
            errs.append("fecha: formato YYYY-MM-DD válido")
        if not valida_turno(body.get("turno")):
            errs.append("turno: 'comida' o 'cena'")
        return errs
    
    
    # ---------------------------------------------------------------------------
    # Límite de peticiones (por teléfono, ventana deslizante en memoria)
    # ---------------------------------------------------------------------------
    
    
    def rate_limited(telefono: str) -> bool:
        now = time.monotonic()
        with _rate_lock:
            dq = _rate.setdefault(telefono, deque())
            while dq and now - dq[0] >= RATE_WINDOW:
                dq.popleft()
            if len(dq) >= RATE_MAX:
                return True
            dq.append(now)
            return False
    
    
    # ---------------------------------------------------------------------------
    # Casos de uso (cada uno es una transacción bajo _lock)
    # ---------------------------------------------------------------------------
    
    --- stderr ---

[turno 8] bash  {"command": "cd /workspace && sed -n '200,340p' app.py"}
    exit=0
    --- stdout ---
    # ---------------------------------------------------------------------------
    
    
    def crear_reserva(body, idem_key):
        errs = errores_creacion(body)
        if errs:
            return 422, {"error": "datos_invalidos", "detalles": errs}
    
        telefono = body["telefono"].strip()
    
        with _lock:
            # Idempotencia: misma clave => misma reserva, nunca dos.
            if idem_key:
                row = DB.execute(
                    "SELECT reserva_id, status FROM idempotencia WHERE clave=?", (idem_key,)
                ).fetchone()
                if row:
                    res = DB.execute(
                        "SELECT * FROM reservas WHERE id=?", (row["reserva_id"],)
                    ).fetchone()
                    if res is not None:
                        status = 201 if res["estado"] == "confirmada" else (
                            202 if res["estado"] == "lista_espera" else 200
                        )
                        return status, to_json(res)
    
            if rate_limited(telefono):
                r
    …(N2: 3370 chars omitidos en medio)…
    stado"] == "confirmada"
            origen = (res["fecha"], res["turno"])
            DB.execute(
                "UPDATE reservas SET personas=?, fecha=?, turno=?, estado='confirmada' WHERE id=?",
                (nuevas_personas, nueva_fecha, nuevo_turno, rid),
            )
            if liberaba_mesa:
                promocionar(*origen)  # su antigua mesa entra en juego
            DB.commit()
            return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
    
    def cancelar_reserva(rid):
        with _lock:
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None or res["estado"] == "cancelada":
                return 404, {"error": "no_encontrada"}
            liberaba_mesa = res["estado"] == "confirmada"
            DB.execute("UPDATE reservas SET estado='cancelada' WHERE id=?", (rid,))
            if liberaba_mesa:
                promocionar(res["fecha"], res["turno"])
            DB.commit()
            return 204, None
    
    
    def disponibilidad(fecha, turno):
        if not valida_fecha(fecha) or not valida_turno(turno):
            return 422, {"error": "datos_invalidos", "detalles": ["fecha y turno válidos requeridos"]}
        with _lock:
            libres = CAPACIDAD - confirmadas(fecha, turno)
        return 200, {"fecha": fecha, "turno": turno, "mesas_libres": max(0, libres)}
    
    
    def reservas_por_telefono(telefono):
        with _lock:
            rows = DB.execute(
                "SELECT * FROM reservas WHERE telefono=? AND estado IN ('confirmada','lista_espera') "
    
    --- stderr ---

[turno 9] bash  {"command": "cd /workspace && grep -n \"def \\|^class \" app.py && sed -n '/def modificar_reserva/,/def cancelar_reserva/p' app.py | head -80"}
    exit=0
    --- stdout ---
    40:def get_db() -> sqlite3.Connection:
    79:def confirmadas(fecha: str, turno: str) -> int:
    87:def posicion_espera(res: sqlite3.Row) -> int:
    97:def promocionar(fecha: str, turno: str) -> None:
    110:def buscar_activa_por_telefono(telefono: str, fecha: str, turno: str, excluir_id=None):
    122:def to_json(res: sqlite3.Row, con_posicion: bool = True) -> dict:
    142:def valida_fecha(v) -> bool:
    152:def valida_personas(v) -> bool:
    156:def valida_turno(v) -> bool:
    160:def valida_str(v) -> bool:
    164:def errores_creacion(body) -> list:
    186:def rate_limited(telefono: str) -> bool:
    203:def crear_reserva(body, idem_key):
    260:def modificar_reserva(rid, body):
    316:def cancelar_reserva(rid):
    329:def disponibilidad(fecha, turno):
    337:def reservas_por_telefono(telefono):
    352:class Handler(BaseHTTPRequestHandler):
    356:    def log_message(self, fmt, *args):  # silencio en producción
    360:    def _send(self, status, payload):
    370:    def _json_body(self):
    383:    def _path_parts(self):
    388: 
    …(N2: 1220 chars omitidos en medio)…
         # Solo cambian personas (o nada): la mesa no varía, estado intacto.
                DB.execute("UPDATE reservas SET personas=? WHERE id=?", (nuevas_personas, rid))
                DB.commit()
                return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
            # Cambio de (fecha, turno): comprobar regla del teléfono y capacidad. Todo o nada.
            otra = buscar_activa_por_telefono(res["telefono"], nueva_fecha, nuevo_turno, excluir_id=rid)
            if otra:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "el teléfono ya tiene una reserva activa en el destino",
                    "reserva": to_json(otra),
                }
            if confirmadas(nueva_fecha, nuevo_turno) >= CAPACIDAD:
                return 409, {
                    "error": "sin_capacidad",
                    "detalle": "el turno de destino está completo; la reserva original no ha cambiado",
                }
    
            liberaba_mesa = res["estado"] == "confirmada"
            origen = (res["fecha"], res["turno"])
            DB.execute(
                "UPDATE reservas SET personas=?, fecha=?, turno=?, estado='confirmada' WHERE id=?",
                (nuevas_personas, nueva_fecha, nuevo_turno, rid),
            )
            if liberaba_mesa:
                promocionar(*origen)  # su antigua mesa entra en juego
            DB.commit()
            return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
    
    def cancelar_reserva(rid):
    
    --- stderr ---

[turno 10] bash  {"command": "cd /workspace && sed -n '79,142p' app.py && sed -n '226,300p' app.py"}
    exit=0
    --- stdout ---
    def confirmadas(fecha: str, turno: str) -> int:
        row = DB.execute(
            "SELECT COUNT(*) c FROM reservas WHERE fecha=? AND turno=? AND estado='confirmada'",
            (fecha, turno),
        ).fetchone()
        return row["c"]
    
    
    def posicion_espera(res: sqlite3.Row) -> int:
        """Posición 1-based en la lista de espera de su (fecha, turno)."""
        row = DB.execute(
            "SELECT COUNT(*) c FROM reservas "
            "WHERE fecha=? AND turno=? AND estado='lista_espera' AND seq<=?",
            (res["fecha"], res["turno"], res["seq"]),
        ).fetchone()
        return row["c"]
    
    
    def promocionar(fecha: str, turno: str) -> None:
        """Si hay hueco, el primero de la lista de espera pasa a confirmada."""
        if confirmadas(fecha, turno) >= CAPACIDAD:
            return
        row = DB.execute(
            "SELECT seq FROM reservas WHERE fecha=? AND turno=? AND estado='lista_espera' "
            "ORDER BY seq LIMIT 1",
            (fecha, turno),
        ).fetchone()
        if row:
            DB.execute("UPDAT
    …(N2: 2883 chars omitidos en medio)…
    o(body["turno"]):
            errs.append("turno: 'comida' o 'cena'")
        if errs:
            return 422, {"error": "datos_invalidos", "detalles": errs}
    
        with _lock:
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None or res["estado"] == "cancelada":
                return 404, {"error": "no_encontrada"}
    
            nueva_fecha = body.get("fecha", res["fecha"])
            nuevo_turno = body.get("turno", res["turno"])
            nuevas_personas = body.get("personas", res["personas"])
            cambia_slot = (nueva_fecha, nuevo_turno) != (res["fecha"], res["turno"])
    
            if not cambia_slot:
                # Solo cambian personas (o nada): la mesa no varía, estado intacto.
                DB.execute("UPDATE reservas SET personas=? WHERE id=?", (nuevas_personas, rid))
                DB.commit()
                return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
            # Cambio de (fecha, turno): comprobar regla del teléfono y capacidad. Todo o nada.
            otra = buscar_activa_por_telefono(res["telefono"], nueva_fecha, nuevo_turno, excluir_id=rid)
            if otra:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "el teléfono ya tiene una reserva activa en el destino",
                    "reserva": to_json(otra),
                }
            if confirmadas(nueva_fecha, nuevo_turno) >= CAPACIDAD:
                return 409, {
                    "error": "sin_capacidad",
    
    --- stderr ---

[turno 11] bash  {"command": "cd /workspace && sed -n '107,145p' app.py && sed -n '203,262p' app.py"}
    exit=0
    --- stdout ---
            DB.execute("UPDATE reservas SET estado='confirmada' WHERE seq=?", (row["seq"],))
    
    
    def buscar_activa_por_telefono(telefono: str, fecha: str, turno: str, excluir_id=None):
        q = (
            "SELECT * FROM reservas WHERE telefono=? AND fecha=? AND turno=? "
            "AND estado IN ('confirmada','lista_espera')"
        )
        args = [telefono, fecha, turno]
        if excluir_id is not None:
            q += " AND id<>?"
            args.append(excluir_id)
        return DB.execute(q, args).fetchone()
    
    
    def to_json(res: sqlite3.Row, con_posicion: bool = True) -> dict:
        d = {
            "id": res["id"],
            "nombre": res["nombre"],
            "telefono": res["telefono"],
            "personas": res["personas"],
            "fecha": res["fecha"],
            "turno": res["turno"],
            "estado": res["estado"],
        }
        if con_posicion and res["estado"] == "lista_espera":
            d["posicion"] = posicion_espera(res)
        return d
    
    
    # ---------------------------------------------------------
    …(N2: 1088 chars omitidos en medio)…
    rate_limited(telefono):
                return 429, {"error": "rate_limit", "detalle": "máximo 20 POST/minuto por teléfono"}
    
            existente = buscar_activa_por_telefono(telefono, body["fecha"], body["turno"])
            if existente:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "ese teléfono ya tiene una reserva activa para esa fecha y turno",
                    "reserva": to_json(existente),
                }
    
            estado = (
                "confirmada"
                if confirmadas(body["fecha"], body["turno"]) < CAPACIDAD
                else "lista_espera"
            )
            rid = str(uuid.uuid4())
            DB.execute(
                "INSERT INTO reservas (id, nombre, telefono, personas, fecha, turno, estado) "
                "VALUES (?,?,?,?,?,?,?)",
                (rid, body["nombre"].strip(), telefono, body["personas"], body["fecha"],
                 body["turno"], estado),
            )
            status = 201 if estado == "confirmada" else 202
            if idem_key:
                DB.execute(
                    "INSERT OR IGNORE INTO idempotencia (clave, reserva_id, status) VALUES (?,?,?)",
                    (idem_key, rid, status),
                )
            DB.commit()
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            return status, to_json(res)
    
    
    def modificar_reserva(rid, body):
        if not isinstance(body, dict):
            return 422, {"error": "datos_invalidos", "detalles": ["body debe ser un objeto JSON"]}
    
    --- stderr ---

[turno 12] bash  {"command": "cd /workspace && sed -n '337,470p' app.py"}
    exit=0
    --- stdout ---
    def reservas_por_telefono(telefono):
        with _lock:
            rows = DB.execute(
                "SELECT * FROM reservas WHERE telefono=? AND estado IN ('confirmada','lista_espera') "
                "ORDER BY seq",
                (telefono,),
            ).fetchall()
            return 200, [to_json(r) for r in rows]
    
    
    # ---------------------------------------------------------------------------
    # HTTP
    # ---------------------------------------------------------------------------
    
    
    class Handler(BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
        server_version = "Marea/1.0"
    
        def log_message(self, fmt, *args):  # silencio en producción
            pass
    
        # -- helpers -----------------------------------------------------------
        def _send(self, status, payload):
            body = b"" if payload is None else json.dumps(payload, ensure_ascii=False).encode()
            self.send_response(status)
            if body:
                self.send_header("Content-Type", "application/json; ch
    …(N2: 1831 chars omitidos en medio)…
    return self._send(*cancelar_reserva(parts[1]))
            self._send(404, {"error": "no_encontrada"})
    
        def do_GET(self):
            parts, qs = self._path_parts()
            if parts == ["disponibilidad"]:
                fecha = (qs.get("fecha") or [None])[0]
                turno = (qs.get("turno") or [None])[0]
                return self._send(*disponibilidad(fecha, turno))
            if parts == ["reservas"]:
                telefono = (qs.get("telefono") or [None])[0]
                if telefono is None:
                    return self._send(
                        422, {"error": "datos_invalidos", "detalles": ["telefono requerido"]}
                    )
                return self._send(*reservas_por_telefono(telefono))
            if len(parts) == 2 and parts[0] == "reservas":
                with _lock:
                    res = DB.execute("SELECT * FROM reservas WHERE id=?", (parts[1],)).fetchone()
                if res is None:
                    return self._send(404, {"error": "no_encontrada"})
                return self._send(200, to_json(res))
            if parts == ["salud"] or parts == []:
                return self._send(200, {"ok": True, "servicio": "marea-reservas", "version": "v1"})
            self._send(404, {"error": "no_encontrada"})
    
    
    def main():
        addr = ("0.0.0.0", int(os.environ.get("PORT", "8080")))
        httpd = ThreadingHTTPServer(addr, Handler)
        print(f"Marea reservas escuchando en {addr[0]}:{addr[1]} (db: {DB_PATH})", flush=True)
        httpd.serve_forever()
    
    
    if __name__ == "__main__":
        main()
    
    --- stderr ---

[turno 13] bash  {"command": "cd /workspace && sed -n '360,412p' app.py"}
    exit=0
    --- stdout ---
        def _send(self, status, payload):
            body = b"" if payload is None else json.dumps(payload, ensure_ascii=False).encode()
            self.send_response(status)
            if body:
                self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            if body:
                self.wfile.write(body)
    
        def _json_body(self):
            try:
                n = int(self.headers.get("Content-Length") or 0)
            except ValueError:
                return None, False
            raw = self.rfile.read(n) if n else b""
            if not raw:
                return None, True  # cuerpo vacío => dict vacío lo decide el caller
            try:
                return json.loads(raw.decode("utf-8")), True
            except (ValueError, UnicodeDecodeError):
                return None, False
    
        def _path_parts(self):
            p = urlparse(self.path)
            return [s for s in p.path.split("/") if s], parse_qs(p.query)
    
        # -- métodos -----------------------------------------------------------
        def do_POST(self):
            parts, _ = self._path_parts()
            if parts == ["reservas"]:
                body, ok = self._json_body()
                if not ok:
                    return self._send(422, {"error": "datos_invalidos", "detalles": ["JSON malformado"]})
                idem = self.headers.get("Idempotency-Key")
                return self._send(*crear_reserva(body if body is not None else {}, idem))
            self._send(404, {"error": "no_encontrada"})
    
        def do_PATCH(self):
            parts, _ = self._path_parts()
            if len(parts) == 2 and parts[0] == "reservas":
                body, ok = self._json_body()
                if not ok:
                    return self._send(422, {"error": "datos_invalidos", "detalles": ["JSON malformado"]})
                return self._send(*modificar_reserva(parts[1], body if body is not None else {}))
            self._send(404, {"error": "no_encontrada"})
    
        def do_DELETE(self):
            parts, _ = self._path_parts()
            if len(parts) == 2 and parts[0] == "reservas":
                return self._send(*cancelar_reserva(parts[1]))
            self._send(404, {"error": "no_encontrada"})
    
    
    --- stderr ---

[turno 14] write_file  {"path": "DECISIONS.md", "content": "<<3034 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 14] write_file  {"path": "test_api.sh", "content": "<<7706 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 15] bash  {"command": "cd /workspace && cat test_api.sh | head -60 && echo ---- && cat DECISIONS.md"}
    exit=0
    --- stdout ---
    #!/bin/bash
    # Pruebas de integración del contrato v1. Requiere la app en localhost:8080.
    set -u
    BASE=http://localhost:8080
    PASS=0; FAIL=0
    
    check() { # desc, esperado, obtenido
      if [ "$2" = "$3" ]; then PASS=$((PASS+1)); else FAIL=$((FAIL+1)); echo "FAIL: $1 (esperado $2, obtenido $3)"; fi
    }
    
    jqget() { python3 -c "import json,sys;d=json.load(sys.stdin);print(d$1)"; }
    
    # --- validación 422 ---
    c=$(curl -s -o /dev/null -w '%{http_code}' -X POST $BASE/reservas -H 'Content-Type: application/json' -d '{"nombre":"A","telefono":"t1","personas":7,"fecha":"2025-12-01","turno":"comida"}')
    check "personas>6 => 422" 422 "$c"
    c=$(curl -s -o /dev/null -w '%{http_code}' -X POST $BASE/reservas -d '{"nombre":"A","telefono":"t1","personas":2,"fecha":"2025-13-01","turno":"comida"}')
    check "fecha invalida => 422" 422 "$c"
    c=$(curl -s -o /dev/null -w '%{http_code}' -X POST $BASE/reservas -d '{"nombre":"A","telefono":"t1","personas":2,"fecha":"2025-12-01","turno":"brunch"}')
    check "tur
    …(N2: 4362 chars omitidos en medio)…
    por PATCH): o pasa todo, o no pasa nada.
    - **Dedup por teléfono**: comprobación de reserva activa (confirmada o en espera)
      para el mismo (telefono, fecha, turno) dentro de la transacción → `409` con la
      reserva existente en el body.
    - **Idempotencia (`Idempotency-Key`)**: tabla `idempotencia(clave → reserva_id)`
      escrita en la misma transacción que la reserva. Un replay devuelve la misma
      reserva (mismo `id`) con su estado actual, y **no consume** cuota de rate limit.
    - **PATCH transaccional**: se valida teléfono + capacidad del destino ANTES de
      escribir; si no cabe → `409` y la original queda intacta. Si la reserva
      confirmada se muda de turno, su mesa liberada promociona la lista de espera
      del turno de origen en la misma transacción.
    - **Rate limit**: ventana deslizante de 60 s en memoria, por teléfono (20 POST).
      Trade-off asumido: al reiniciar la app el contador se resetea; es un límite
      anti-abuso, no un dato de negocio, así que no merece persistencia.
    
    ## Trade-offs asumidos
    
    - **Un lock global** en lugar de locking fino: con esta carga (una sala, un panel)
      la serialización total es el diseño más simple que es *demostrablemente* correcto.
    - **Sin framework**: menos ergonomía a cambio de cero dependencias. El contrato
      v1 está congelado, así que el coste de rutas manuales es bajo y acotado.
    - **`posicion` se calcula al vuelo** (COUNT sobre `seq`), no se almacena: siempre
      es consistente aunque haya promociones o cancelaciones intermedias.
    
    --- stderr ---

[turno 16] bash  {"command": "cd /workspace && rm -rf data && chmod +x run.sh test_api.sh && (sh run.sh > /tmp/app.log 2>&1 &) && sleep 1.5 && curl -s http://localhost:8080/salud && bash test_api.sh"}
    exit=127
    --- stdout ---
    
    --- stderr ---
    sh: 1: curl: not found

[turno 17] write_file  {"path": "test_api.py", "content": "<<9416 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 18] bash  {"command": "cd /workspace && head -80 test_api.py"}
    exit=0
    --- stdout ---
    #!/usr/bin/env python3
    """Pruebas de integración del contrato v1 contra localhost:8080."""
    import json
    import subprocess
    import sys
    import time
    import urllib.request
    import urllib.error
    
    BASE = "http://localhost:8080"
    PASS = FAIL = 0
    
    
    def req(method, path, body=None, headers=None):
        data = json.dumps(body).encode() if body is not None else None
        r = urllib.request.Request(BASE + path, data=data, method=method,
                                   headers=headers or {})
        if data:
            r.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(r) as resp:
                raw = resp.read()
                return resp.status, json.loads(raw) if raw else None
        except urllib.error.HTTPError as e:
            raw = e.read()
            return e.code, (json.loads(raw) if raw else None)
    
    
    def check(desc, expected, got):
        global PASS, FAIL
        if expected == got:
            PASS += 1
        else:
            FAIL += 1
            print(f"FAIL: {desc} (espe
    …(N2: 1210 chars omitidos en medio)…
    5-13-01", "turno": "comida"}, "fecha invalida"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-1", "turno": "comida"}, "fecha mal formada"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "brunch"}, "turno desconocido"),
        ({"telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "comida"}, "falta nombre"),
        ({"nombre": "A", "personas": 2, "fecha": "2025-12-01", "turno": "cena"}, "falta telefono"),
    ]:
        s, _ = req("POST", "/reservas", body)
        check(f"422 {desc}", 422, s)
    
    FECHA = "2025-12-01"
    
    # --- capacidad: 14 confirmadas, 15ª y 16ª a lista de espera ---
    ids = []
    for i in range(14):
        s, b = req("POST", "/reservas", {"nombre": f"C{i}", "telefono": f"tel-{i}",
                                         "personas": 4, "fecha": FECHA, "turno": "cena"})
        check(f"reserva {i+1} => 201", 201, s)
        check(f"reserva {i+1} confirmada", "confirmada", b["estado"])
        ids.append(b["id"])
    
    s, w1 = req("POST", "/reservas", {"nombre": "W1", "telefono": "tel-w1",
                                      "personas": 2, "fecha": FECHA, "turno": "cena"})
    check("15a => 202", 202, s)
    check("15a lista_espera", "lista_espera", w1["estado"])
    check("15a posicion 1", 1, w1["posicion"])
    s, w2 = req("POST", "/reservas", {"nombre": "W2", "telefono": "tel-w2",
                                      "personas": 2, "fecha": FECHA, "turno": "cena"})
    check("16a => 202", 202, s)
    check("16a posicion 2", 2, w2["posicion"])
    
    --- stderr ---

[turno 19] bash  {"command": "cd /workspace && sed -n '80,220p' test_api.py"}
    exit=0
    --- stdout ---
    check("16a posicion 2", 2, w2["posicion"])
    
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check("disponibilidad llena", 0, b["mesas_libres"])
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=comida")
    check("comida libre", 14, b["mesas_libres"])
    
    # --- dedup por teléfono (409 con reserva en body) ---
    s, b = req("POST", "/reservas", {"nombre": "C0bis", "telefono": "tel-0",
                                     "personas": 2, "fecha": FECHA, "turno": "cena"})
    check("telefono duplicado => 409", 409, s)
    check("409 incluye reserva", ids[0], b["reserva"]["id"])
    # mismo teléfono, otro turno: permitido
    s, b = req("POST", "/reservas", {"nombre": "C0", "telefono": "tel-0",
                                     "personas": 2, "fecha": FECHA, "turno": "comida"})
    check("mismo tel otro turno => 201", 201, s)
    req("DELETE", f"/reservas/{b['id']}")
    # dedup también con lista de espera
    s, b = req("POST", "/reservas", {"nombre": "W1bis", "telefono": "tel-w1",
               
    …(N2: 3313 chars omitidos en medio)…
    = []
    for i in range(22):
        s, _ = req("POST", "/reservas", {"nombre": "R", "telefono": "tel-rl", "personas": 2,
                                         "fecha": "2026-01-01", "turno": "comida"})
        codes.append(s)
    check("1a del ratelimit => 201", 201, codes[0])
    check("2a-20a => 409 (dup)", {409}, set(codes[1:20]))
    check("21a y 22a => 429", {429}, set(codes[20:]))
    # otro teléfono NO bloqueado
    s, _ = req("POST", "/reservas", {"nombre": "R2", "telefono": "tel-rl-2", "personas": 2,
                                     "fecha": "2026-01-01", "turno": "comida"})
    check("otro telefono no bloqueado", 201, s)
    
    # --- durabilidad: reiniciar la app y comprobar que todo sigue ---
    print("Reiniciando app para probar durabilidad...")
    subprocess.run(["pkill", "-f", "app.py"])
    time.sleep(1)
    subprocess.Popen(["sh", "run.sh"], stdout=open("/tmp/app2.log", "w"),
                     stderr=subprocess.STDOUT, cwd=".")
    time.sleep(1.5)
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("tras reinicio: reserva sigue", "confirmada", b["estado"])
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check("tras reinicio: disponibilidad", 0, b["mesas_libres"])
    s2, b2 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    check("tras reinicio: idempotencia persiste", b1["id"], b2["id"])
    
    print(f"\n{PASS} OK, {FAIL} FAIL")
    sys.exit(1 if FAIL else 0)
    
    --- stderr ---

[turno 20] bash  {"command": "cd /workspace && sed -n '100,175p' test_api.py"}
    exit=0
    --- stdout ---
    check("dup en espera => 409", 409, s)
    
    # --- idempotencia ---
    key = "clave-abc-123"
    s1, b1 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    s2, b2 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    check("idem 1a => 201", 201, s1)
    check("idem replay mismo id", b1["id"], b2["id"])
    s, lst = req("GET", "/reservas?telefono=tel-idem")
    check("idem: solo UNA reserva", 1, len(lst))
    
    # --- GET /reservas?telefono ---
    s, lst = req("GET", "/reservas?telefono=tel-w1")
    check("activas de tel-w1", 1, len(lst))
    check("estado espera en listado", "lista_espera", lst[0]["estado"])
    s, lst = req("GET", "/reservas?telefono=no-existe")
    check("telefono sin reservas => []", [], lst)
    
    # --- PATCH -
    …(N2: 1175 chars omitidos en medio)…
    reservas/{bY['id']}", {"fecha": "2025-12-03"})
    check("patch viola tel => 409", 409, s)
    # patch id inexistente => 404
    s, _ = req("PATCH", "/reservas/no-existe", {"personas": 2})
    check("patch inexistente => 404", 404, s)
    
    # --- DELETE + promoción FIFO ---
    s, _ = req("DELETE", f"/reservas/{ids[0]}")
    check("delete => 204", 204, s)
    s, _ = req("DELETE", f"/reservas/{ids[0]}")
    check("segundo delete => 404", 404, s)
    s, b = req("GET", f"/reservas/{w1['id']}")
    check("W1 promocionada", "confirmada", b["estado"])
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("W2 sigue en espera", "lista_espera", b["estado"])
    check("W2 ahora posicion 1", 1, b["posicion"])
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check("sigue llena tras promocion", 0, b["mesas_libres"])
    # mover confirmada fuera promociona a W2
    s, b = req("PATCH", f"/reservas/{ids[2]}", {"fecha": "2025-12-05"})
    check("patch mueve de turno => 200", 200, s)
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("W2 promocionada tras patch", "confirmada", b["estado"])
    
    # --- rate limit: 20 POST/min por teléfono ---
    codes = []
    for i in range(22):
        s, _ = req("POST", "/reservas", {"nombre": "R", "telefono": "tel-rl", "personas": 2,
                                         "fecha": "2026-01-01", "turno": "comida"})
        codes.append(s)
    check("1a del ratelimit => 201", 201, codes[0])
    check("2a-20a => 409 (dup)", {409}, set(codes[1:20]))
    check("21a y 22a => 429", {429}, set(codes[20:]))
    # otro teléfono NO bloqueado
    
    --- stderr ---

[turno 21] bash  {"command": "cd /workspace && sed -n '122,140p' test_api.py"}
    exit=0
    --- stdout ---
    # --- PATCH ---
    # mover una confirmada a turno lleno => 409 y original intacta
    s, b = req("PATCH", f"/reservas/{ids[1]}", {"turno": "cena", "fecha": FECHA})
    check("patch no-op mismo slot => 200", 200, s)
    sX, bX = req("POST", "/reservas", {"nombre": "X", "telefono": "tel-x", "personas": 2,
                                       "fecha": "2025-12-03", "turno": "comida"})
    s, b = req("PATCH", f"/reservas/{bX['id']}", {"fecha": FECHA, "turno": "cena"})
    check("patch a turno lleno => 409", 409, s)
    s, b = req("GET", f"/reservas/{bX['id']}")
    check("original intacta fecha", "2025-12-03", b["fecha"])
    check("original intacta estado", "confirmada", b["estado"])
    # patch personas inválidas => 422, intacta
    s, _ = req("PATCH", f"/reservas/{bX['id']}", {"personas": 9})
    check("patch personas invalidas => 422", 422, s)
    s, b = req("PATCH", f"/reservas/{bX['id']}", {"personas": 6})
    check("patch personas => 200", 200, s)
    check("personas actualizadas", 6, b["personas"])
    # patch que viola regla del teléfono => 409
    sY, bY = req("POST", "/reservas", {"nombre": "Y", "telefono": "tel-x", "personas": 2,
    
    --- stderr ---

[turno 22] bash  {"command": "cd /workspace && rm -rf data && rm -f test_api.sh && pkill -f app.py; sleep 0.5; nohup sh run.sh > /tmp/app.log 2>&1 & sleep 1.5 && python3 test_api.py; echo \"exit=$?\"; tail -5 /tmp/app.log /tmp/app2.log 2>/dev/null"}
    exit=1
    --- stdout ---
    exit=1
    
    --- stderr ---
    sh: 1: pkill: not found
      File "/workspace/test_api.py", line 47
        ], start=None if False else []:
                ^
    SyntaxError: invalid syntax

[turno 23] bash  {"command": "cd /workspace && sed -n '36,60p' test_api.py"}
    exit=0
    --- stdout ---
    
    
    # --- validación 422 ---
    for body, desc in [
        ({"nombre": "A", "telefono": "t1", "personas": 7, "fecha": "2025-12-01", "turno": "comida"}, "personas>6"),
        ({"nombre": "A", "telefono": "t1", "personas": 0, "fecha": "2025-12-01", "turno": "comida"}, "personas<1"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-13-01", "turno": "comida"}, "fecha invalida"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-1", "turno": "comida"}, "fecha mal formada"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "brunch"}, "turno desconocido"),
        ({"telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "comida"}, "falta nombre"),
        ({"nombre": "A", "personas": 2, "fecha": "2025-12-01", "turno": "cena"}, "falta telefono"),
    ], start=None if False else []:
        pass
    for body, desc in [
        ({"nombre": "A", "telefono": "t1", "personas": 7, "fecha": "2025-12-01", "turno": "comida"}, "personas>6"),
        ({"nombre": "A", "telefono": "t1", "personas": 0, "fecha": "2025-12-01", "turno": "comida"}, "personas<1"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-13-01", "turno": "comida"}, "fecha invalida"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-1", "turno": "comida"}, "fecha mal formada"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "brunch"}, "turno desconocido"),
        ({"telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "comida"}, "falta nombre"),
        ({"nombre": "A", "personas": 2, "fecha": "2025-12-01", "turno": "cena"}, "falta telefono"),
    ]:
        s, _ = req("POST", "/reservas", body)
        check(f"422 {desc}", 422, s)
    
    
    --- stderr ---

[turno 24] bash  {"command": "cd /workspace && python3 - <<'EOF'\nimport re\nsrc = open('test_api.py').read()\n# eliminar bloque duplicado defectuoso (termina en '], start=None if False else []:\\n    pass\\n')\ni = src.find('# --- validación 422 ---')\nj = src.find('for body, desc in [', src.find('pass\\n', i))\nsrc = src[:src.find('for body, desc in [', i)] + src[j:] if 'start=None' in src else src\n# reemplazar pkill por kill via /proc\nsrc = src.replace('''subprocess.run([\"pkill\", \"-f\", \"app.py\"])''', '''import os, signal\nfor pid in os.listdir(\"/proc\"):\n    if pid.isdigit() and pid != str(os.getpid()):\n        try:\n            cmd = open(f\"/proc/{pid}/cmdline\",\"rb\").read().decode(errors=\"ignore\")\n            if \"app.py\" in cmd:\n                os.kill(int(pid), signal.SIGTERM)\n        except (OSError, IOError):\n            pass''')\nopen('test_api.py','w').write(src)\nEOF\npython3 -c \"import ast; ast.parse(open('/workspace/test_api.py').read())\" && echo SYNTAX_OK"}
    exit=0
    --- stdout ---
    SYNTAX_OK
    
    --- stderr ---

[turno 25] bash  {"command": "cd /workspace && kill $(cat /tmp/app.pid 2>/dev/null) 2>/dev/null; for p in /proc/[0-9]*; do grep -qa app.py $p/cmdline 2>/dev/null && kill ${p#/proc/} 2>/dev/null; done; sleep 0.5; rm -rf data; nohup sh run.sh > /tmp/app.log 2>&1 & sleep 1.5 && python3 test_api.py; echo \"exit=$?\""}
    exit=143
    --- stdout ---
    
    --- stderr ---

[turno 26] bash  {"command": "cd /workspace && python3 - <<'EOF'\nsrc = open('test_api.py').read()\nsrc = src.replace('if \"app.py\" in cmd:', 'if \"app.py\" in cmd.split(\"\\\\x00\"):')\nopen('test_api.py','w').write(src)\nEOF\ngrep -n 'app.py\" in' test_api.py"}
    exit=0
    --- stdout ---
    177:            if "app.py" in cmd.split("\x00"):
    
    --- stderr ---

[turno 27] bash  {"command": "cd /workspace && for p in /proc/[0-9]*; do tr '\\0' ' ' < $p/cmdline 2>/dev/null | grep -q '^python3 app.py' && kill ${p#/proc/} 2>/dev/null; done; sleep 0.5; rm -rf data; nohup sh run.sh > /tmp/app.log 2>&1 & sleep 1.5 && python3 test_api.py; echo \"exit=$?\""}
    exit=0
    --- stdout ---
    Reiniciando app para probar durabilidad...
    
    76 OK, 0 FAIL
    exit=0
    
    --- stderr ---

[turno 28] bash  {"command": "cd /workspace && python3 - <<'EOF'\nimport json, urllib.request, concurrent.futures as cf\ndef post(i):\n    d = json.dumps({\"nombre\":f\"P{i}\",\"telefono\":f\"cc-{i}\",\"personas\":2,\n                    \"fecha\":\"2027-05-05\",\"turno\":\"comida\"}).encode()\n    r = urllib.request.Request(\"http://localhost:8080/reservas\", data=d,\n        headers={\"Content-Type\":\"application/json\"}, method=\"POST\")\n    try:\n        with urllib.request.urlopen(r) as resp: return resp.status\n    except urllib.error.HTTPError as e: return e.code\nwith cf.ThreadPoolExecutor(30) as ex:\n    codes = list(ex.map(post, range(30)))\nprint(\"201:\", codes.count(201), \"202:\", codes.count(202))\nwith urllib.request.urlopen(\"http://localhost:8080/disponibilidad?fecha=2027-05-05&turno=comida\") as r:\n    print(json.load(r))\nEOF"}
    exit=0
    --- stdout ---
    201: 14 202: 16
    {'fecha': '2027-05-05', 'turno': 'comida', 'mesas_libres': 0}
    
    --- stderr ---

[turno 29] bash  {"command": "cd /workspace && for p in /proc/[0-9]*; do tr '\\0' ' ' < $p/cmdline 2>/dev/null | grep -q '^python3 app.py' && kill ${p#/proc/} 2>/dev/null; done; sleep 0.5; rm -rf data; timeout 3 sh run.sh & sleep 1.2; python3 - <<'EOF'\nimport json, urllib.request\nwith urllib.request.urlopen(\"http://localhost:8080/disponibilidad?fecha=2025-01-01&turno=comida\") as r:\n    print(\"OK arranque limpio:\", json.load(r))\nEOF\nwait; ls -la /workspace; echo \"---\"; head -3 /workspace/DECISIONS.md"}
    exit=0
    --- stdout ---
    Marea reservas escuchando en 0.0.0.0:8080 (db: /workspace/data/reservas.db)
    OK arranque limpio: {'fecha': '2025-01-01', 'turno': 'comida', 'mesas_libres': 14}
    total 60
    drwxr-xr-x 3 root root  4096 Jul 13 12:19 .
    drwxr-xr-x 1 root root  4096 Jul 13 12:12 ..
    -rw-r--r-- 1 root root  3034 Jul 13 12:16 DECISIONS.md
    -rw-r--r-- 1 root root  2399 Jul 13 12:12 README.md
    -rw-r--r-- 1 root root  2943 Jul 13 12:12 api.md
    -rw-r--r-- 1 root root 16401 Jul 13 12:14 app.py
    drwxr-xr-x 2 root root  4096 Jul 13 12:19 data
    -rwxr-xr-x 1 root root   124 Jul 13 12:14 run.sh
    -rw-r--r-- 1 root root  8885 Jul 13 12:18 test_api.py
    ---
    # DECISIONS.md
    
    ## Stack: Python 3 (solo stdlib) + SQLite
    
    --- stderr ---

=== Respuesta final del modelo ===
Sistema completo y verificado. Resumen de lo entregado:

**Stack**: Python 3 (solo stdlib: `http.server` + `sqlite3`) — cero dependencias, funciona sin red, y SQLite con WAL garantiza durabilidad y transacciones.

**Ficheros**:
- `app.py` — la aplicación completa (dominio + HTTP)
- `run.sh` — ejecutable, arranca en `0.0.0.0:8080` en primer plano
- `DECISIONS.md` — justificación del stack y las decisiones (persistencia, lock global, idempotencia, rate limit en memoria, trade-offs)
- `test_api.py` — suite de integración del contrato

**Cumplimiento del contrato v1 verificado (76 checks OK)**:
- Capacidad estricta: máx. 14 confirmadas por (fecha, turno); la 15ª → `202` con `posicion` (probado también bajo 30 POST concurrentes: exactamente 14 confirmadas)
- Lista de espera FIFO con promoción automática en DELETE y en PATCH que libera mesa
- Dedup de teléfono por (fecha, turno) → `409` con la reserva existente en `"reserva"`
- `Idempotency-Key`: misma clave → misma reserva (mismo `id`), persistente tras reinicio, sin consumir rate limit
- PATCH transaccional: si el destino no cabe o viola la regla del teléfono → `409` y la original queda intacta
- Validaciones `422` (personas 1-6, fecha `YYYY-MM-DD` real, turno, campos faltantes)
- Rate limit: 20 POST/min por teléfono → `429`, sin bloquear otros teléfonos
- Durabilidad: la suite mata la app, la rearranca con `run.sh` y comprueba que reservas, disponibilidad e idempotencia sobreviven

LISTO

[stats] done=True budget_exceeded=False turns=30 tokens=1067+26602 wallclock=433.5s