Plan de Migración AmputatorBot → Devvit v7
Estado: borrador vivo — M1–M5 enviados, M6 (despliegue en la nube + lanzamiento) en progreso. Redactado: 2026-05-25 Límite presupuestario: €15/mes en total
Estilo de trabajo — LÉE ESTO PRIMERO
Al desarrollador le gusta ser consultado sobre decisiones arquitectónicas antes de que se escriba código. Para cualquier elección que sea:
- Sobre qué librería, framework o patrón usar
- Sobre el diseño del modelo de datos o decisiones de esquema
- Sobre cómo estructurar un nuevo módulo o capa
- Sobre compensaciones entre costo y complejidad
- Sobre desviarse de este plan de forma no trivial
…detente y pregunta antes de proceder. Presenta las opciones, explica las compensaciones honestamente (incluyendo objeciones si no estás de acuerdo) y espera una decisión. El trabajo de implementación mecánica no necesita consulta; la forma arquitectónica sí.
Acciones externas que el desarrollador hace manualmente, no Claude:
- Ejecutar consultas contra las bases de datos MySQL/Postgres de producción (Claude puede escribir el SQL; el desarrollador lo ejecuta)
- Registrarse en servicios (Scaleway, cuenta de desarrollador de Reddit, envío al Directorio de Apps, registro en el programa de recompensas)
- Cualquier cosa que toque una cuenta externa con la identidad del desarrollador (inicios de sesión OAuth,
devvit login,scw init, cambios de facturación) - Cambios de DNS en Cloudflare
- Detener/iniciar el bot de producción en PythonAnywhere
git pusha repos remotos que afectan a producción- Publicar la App de Devvit (
devvit upload --publish)
Claude prepara los comandos exactos y le dice al desarrollador cuándo ejecutarlos. Leer código, escribir código, ejecutar pruebas locales, builds locales de Docker, cargo/npm local — Claude hace todo eso.
Contexto del proyecto
Qué es AmputatorBot: Un bot de Reddit que detecta URLs AMP en comentarios/publicaciones/mensajes privados y responde con el enlace canónico (no AMP). Mantenido por el desarrollador (u/Killed_Mufasa). Vive en https://github.com/jvdburgh/AmputatorBot. ~181 estrellas de GitHub. Funciona a escala. El programa de migración de la Plataforma de Desarrolladores de Reddit ofrece $1,000 a los bots que migren de la Data API a Devvit antes del 31 de diciembre de 2026.
Qué se migra:
- Bot de Reddit (Python + PRAW) → App Devvit (TypeScript,
@devvit/web) - API HTTP (Flask) → Rust (Axum + puerto de Mozilla-Readability + sqlx)
- Sitio web (Flask + Bootstrap) → Astro 5 + Tailwind 4 + shadcn/ui
- Base de datos (MySQL en PythonAnywhere) → Postgres 17 en Scaleway Managed Database
- Alojamiento (PythonAnywhere) → Scaleway (París, Francia — nube gestionada de origen UE)
Cambio en el modelo arquitectónico: Devvit es opt-in por subreddit, no global. Los moderadores instalan la app por subreddit; no puede ver contenido donde no está instalada. Las respuestas se publican como identidad de la app por instalación, no como u/AmputatorBot. Aceptado. Caída estimada en el volumen de respuestas: 30–60%.
Objetivos (declarados por el usuario):
- Stack moderno, aprender cosas en el camino (Rust, Devvit, Astro/Tailwind4/shadcn)
- El contrato API se mantiene retrocompatible
- Durante la reescritura (M1–M5), el bot antiguo seguía funcionando en PythonAnywhere para que el nuevo pudiera romperse libremente sin consecuencias visibles para el usuario. El bot antiguo se retira en el corte de M6 — sin ejecución paralela permanente.
- Aplastar la reescritura; preservar el código antiguo en
praw-python-archive/
Decisiones bloqueadas
| Ruta | Opt-in por subreddit. Corte directo en M6 (bot antiguo mantenido vivo durante el desarrollo de M1–M5 solamente) |
| SDK Devvit | @devvit/web (modelo de servidor moderno) |
| Config Devvit | devvit.json (formato oficial con $schema) |
| Andamio Devvit | reddit/devvit-template-bare, sin UI de publicaciones/menús |
| Lenguaje backend | Rust |
| Framework backend | Axum 0.8 (Tokio + Hyper) |
| Extracción de artículos | crate dom_smoothie (≈0.17, puerto Rust de Readability que sigue de cerca el readability.js de Mozilla, mantenido activamente a partir de 2026-03) |
| Análisis HTML | crate scraper (construido sobre html5ever) |
| Cliente HTTP | reqwest |
| Driver Postgres | sqlx (consultas verificadas en tiempo de compilación) |
| Base de datos | Postgres 17 en Scaleway Managed Database (DB DEV-S o un nivel superior, dimensionada para ~2GB + margen de crecimiento). 1.7M filas confirmadas. |
| Sitio web | Astro 5 + Tailwind 4 + shadcn/ui |
| Alojamiento | Scaleway Serverless Containers (París/AMS, nube francesa de origen UE) |
| Servicio único | Un binario Rust sirve /api/* y contenido estático de Astro via tower-http::ServeDir |
| Observabilidad | Solo Scaleway Cockpit (registros y métricas integrados) inicialmente |
| Alojamiento de código | GitHub (monorepo existente) |
| DNS | Cloudflare (existente) — también maneja limitación de velocidad |
| Auth API | Ninguna en ninguna ruta — completamente abierto. Cloudflare maneja el abuso. |
| Limitación de velocidad | Solo Cloudflare, sin middleware Rust |
| Dominio | Único — todo en www.amputatorbot.com |
| Subreddit de prueba | r/test |
| Herramientas IA | Claude Code + Devvit MCP + JetBrains MCP en IntelliJ |
| Exclusión voluntaria por usuario | Eliminada. Los usuarios bloquean la cuenta del bot; la identidad de app por instalación hace que la exclusión basada en bloqueo funcione a nivel de instalación. |
| Exclusión voluntaria por subreddit | Eliminada (los moderadores desinstalan o desactivan autoReply en la configuración de instalación) |
| Modmail / MDs | Ninguno. Sin manejador de modmail entrante, sin modmail de bienvenida, sin MDs — la insignia de "App" de Reddit maneja la divulgación del bot, y la lista del Directorio de Apps de Devvit cubre la incorporación. |
| Umbral de karma | Ninguno |
| Remitente del Directorio de Apps | u/Killed_Mufasa |
Por qué estas elecciones
La razón detrás de las decisiones anteriores. Cada una fue una selección deliberada frente a una alternativa creíble.
-
Rust + Axum sobre Node/TS o Python. Binario estático único, sin tiempo de ejecución, sin virtualenv, sin
node_modules. Las partes difíciles de Rust (lifetimes, unsafe, genéricos avanzados) realmente no aparecen en este tipo de servicio (fetch HTTP + regex + consulta HTML + escritura DB), así que la curva de aprendizaje es principalmenteResult/?, asíncrono/Tokio y macros de sqlx — todo vale la pena saberlo. Axum 0.8 es el framework de consenso en 2026 (Tokio + Hyper + middleware Tower). sqlx proporciona consultas verificadas en tiempo de compilación. -
dom_smoothiesobrers-trafilaturay otros puertos de Readability. El bot existente usanewspaper3k+difflib.SequenceMatcherpara puntuación de confianza por adivinación y verificación (helpers/article_comparer.py). Para el puerto Rust queremos un extractor alineado con Mozilla-Readability. Los candidatos a mayo de 2026:dom_smoothie0.17 (último lanzamiento 2026-03, ~40k descargas recientes, reponiklak/dom_smoothie) — sigue de cercareadability.js, dos modos (imitación Mozilla + personalizado), análisis de metadatos JSON-LD + OG, mantenido activamente.readability0.3 de kumabook (último lanzamiento 2023-12, estancado) — puerto del Readability original de arc90 (anterior a la bifurcación de Mozilla). Mayor número histórico de descargas pero sin mantenimiento.readable-readability0.4 (último lanzamiento 2022-12, estancado).llm_readability0.0.17 (activo pero 0.0.x — demasiado temprano para producción).
dom_smoothiegana por mantenimiento activo + linaje explícito dereadability.jsde Mozilla. Usamos su modo estándar (imitación Mozilla) para puntuación de similitud. No se necesita spike; fijar adom_smoothie = "0.17"en M2. -
Sin autenticación, en ninguna parte. La producción actualmente tiene
authorization_required=Falseen/convert(AmputatorBotCom/main.py:162). Nadie depende de autenticación bearer, y con la exclusión voluntaria eliminada, el backend no tiene rutas privilegiadas en absoluto. Cloudflare maneja el abuso. -
Limitación de velocidad del lado de Cloudflare. Cloudflare ya está al frente del dominio. Las reglas por IP allí son código Rust cero y configurables sin redeploy.
-
Sin mecanismo de exclusión voluntaria, sin modmail, sin MDs. Los usuarios y moderadores que no quieran el bot pueden bloquear la identidad de la app por instalación o desinstalar la app respectivamente. Eso es suficiente — construir un registro de exclusión separado encima es un mecanismo que no necesitamos. Esto elimina el activador de modmail, el modmail de bienvenida, las rutas privilegiadas con autenticación bearer, la tabla
optouts, la caché de exclusión Redis y la ruta heredada de "invocado por MD" de un solo golpe. -
Modernizar el esquema de DB durante la migración. La herramienta de migración se ejecuta una vez. Limpiar
VARCHAR(4000)→TEXT, agregar los índices que faltan enmodels/table.pyy eliminar columnas muertas es barato de hacer una vez y nunca más. -
Observabilidad solo de registros inicialmente. Scaleway Cockpit (el flujo integrado de registros y métricas) es suficiente para hacer grep cuando surjan problemas. Agregar Sentry/Bugsink solo si el grep de registros se vuelve doloroso.
-
Pasar los métodos existentes de detección AMP + canónicos, pero corregir falsos positivos en su lugar. Los 14 patrones de subcadena (
static/static.py) son ruidosos (entidades HTML&,lamp_post,streaming_amplifier). Los 10 métodos canónicos y su orden enhelpers/canonical_methods.pyestán bien ajustados y se mantienen. Las mejoras ocurren en la capa de detección AMP (límites de palabras, alcance a componentes de URL después de la extracción) y en listas blancas de ruta rápida para marcadores de alta señal. -
Actualizar el markdown de respuesta del bot. Un pase de diseño único en una nueva plantilla, aplicado en la migración. No necesita verse idéntico al bot antiguo.
-
git mvtodo lo antiguo apraw-python-archive/en un solo commit. Aplastar. El bot PRAW antiguo sigue funcionando en PythonAnywhere durante M1–M5 para que el nuevo lado pueda romperse libremente; se detiene en el corte de M6. -
Seis hitos, sin calendario. Local primero: M1–M5 son todo desarrollo local (andamio, motor canónico, paridad API, Astro, prueba de juego Devvit). M6 es la nube + lanzamiento público (aprovisionamiento de Scaleway, migración de datos de producción, corte de DNS, Directorio de Apps, retiro del bot antiguo). Sin fecha límite externa (la recompensa es diciembre de 2026). El bot antiguo cubre la producción hasta el corte. Aprender Rust + Devvit + Astro es parte del objetivo, así que el ritmo debe coincidir con la comprensión, no con un cronograma.
Arquitectura
┌─────────────────────────────────────────────┐
│ Subreddit X (mod instaló amputatorbot) │
│ Comentarios / publicaciones │
└──────────────┬──────────────────────────────┘
│ Desencadenadores Devvit (HTTP POST a rutas internas)
▼
┌─────────────────────────────────────────────┐
│ App Devvit (Node, @devvit/web) │
│ Se ejecuta en infraestructura de Reddit │
│ │
│ Rutas HTTP internas: │
│ /internal/triggers/comment-submit │
│ /internal/triggers/post-submit │
│ │
│ Redis integrado de Devvit: │
│ handled:{commentId} TTL 1h │
│ │
│ Configuración por instalación: │
│ autoReply, customFooter, killSwitch │
│ │
│ Lista blanca (devvit.json): │
│ http.domains: ["www.amputatorbot.com"] │
└──────────────┬──────────────────────────────┘
│ HTTPS (sin auth):
│ GET /api/v1/convert?q=...&gac=true&md=3
▼
┌─────────────────────────────────────────────┐
│ Cloudflare (DNS + limitación de velocidad + WAF) │
└──────────────┬──────────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ Scaleway (París/AMS, UE) │
│ www.amputatorbot.com │
│ │
│ Backend Rust (Axum 0.8) — binario único │
│ /api/v1/convert (contrato existente) │
│ /api/v2/convert (JSON en camelCase) │
│ /api/v2/stats (conteo en caché) │
│ /api/v2/health │
│ fallback → Astro estático (ServeDir) │
│ │
│ Búsqueda canónica (10 métodos, mismo │
│ orden que la impl. Python): │
│ REL, CANURL, OG_URL, │
│ GOOGLE_MANUAL_REDIRECT, GOOGLE_JS_…, │
│ BING_ORIGINAL_URL, SCHEMA_MAINENTITY, │
│ TCO_PAGETITLE, META_REDIRECT, │
│ GUESS_AND_CHECK (confianza de similitud│
│ de artículo via Mozilla Readability),│
│ DATABASE (búsqueda en caché) │
│ │
│ Postgres 17 (Scaleway Managed Database): │
│ links │
└─────────────────────────────────────────────┘
Por qué un solo servicio Rust que sirve API + sitio estático: La lista blanca de Devvit es a nivel de dominio. Un solo contenedor Serverless de Scaleway mantiene la historia de despliegue simple. tower-http::ServeDir como fallback son ~5 líneas.
Contrato API
Dos endpoints se envían lado a lado:
GET /api/v1/convert— contrato heredado preservado (cadena de consulta snake_case, respuesta JSON snake_case). Solo GET ya que el bot heredado siempre estaba basado en cadena de consulta; POST es solo para v2. Obsoleto para nuevos llamadores pero soportado indefinidamente para que los servicios externos no se rompan.POST /api/v2/convert— contrato moderno: cuerpo JSON en camelCase, respuesta JSON en camelCase.entryTypees un campo del cuerpo en lugar de un encabezado. Validación estricta (campos desconocidos → 422). Recomendado para Devvit (M5) + sitio web (M4) + nuevos llamadores externos.
Cada escritura en la caché links registra api_version (1 o 2 — NULL para las filas importadas del bot heredado pre-v7) para que la adopción sea consultable: SELECT api_version, COUNT(*) FROM links GROUP BY 1.
v1 — GET /api/v1/convert (de AmputatorBotCom/main.py:161+)
Parámetros de consulta:
q(obligatorio) — URL o texto que contiene URLsgac— adivinación y verificación, por defectotruemd— profundidad máxima, por defectostatic.MAX_DEPTH(pasar el valor)gc— generar markdown de respuesta en la respuesta, por defectofalser— modo de redirección (303 a canónico en lugar de devolver JSON), por defectofalse
Comportamiento de codificación URL — ambos deben funcionar
La API soporta dos llamadores: clientes bien comportados que codifican sus URLs con porcentaje, y humanos que pegan URLs sin codificar en la barra de direcciones del navegador. El contrato v7:
- Las URLs codificadas siempre funcionan, independientemente de la posición de
qen la lista de parámetros.?q=https%3A%2F%2F...&gac=truey?gac=true&q=https%3A%2F%2F...son ambos válidos. - Las URLs sin codificar funcionan cuando
qes el último parámetro.?gac=true&q=https://example.eu/article?id=1está bien — los parámetros finales de la URL (?id=1) se conservan.qentre otros parámetros conocidos es
…
[truncado para traducción]