AIREITER

329 comandos, 128 pendientes: reconcilia una migración con teoría de conjuntos, no con un modelo

Última actualización: 2026-07-31 07:49:50

Una función en Go entra en un modelo y sale convertida en Python limpio e idiomático, con nombres que encajan con el proyecto y tests en verde. Repite el proceso doscientas veces y es fácil dar la migración por cerrada. Pero una migración real puede seguir teniendo 128 comandos sin trasladar.

El problema es confundir «traducido» con «migrado». Validar la traducción de una función concreta es precisamente una de las tareas que mejor resuelve un modelo. En cambio, decidir si el conjunto completo ha migrado pertenece a otra categoría: es una operación de conjuntos. Y ahí un modelo no debería intervenir, porque es justo donde más fácilmente puede darte una falsa sensación de completitud.

Este es el balance de una migración real de Go a Python y la razón por la que debe cerrarlo un script. El modelo puede explicar el resultado, pero no calcularlo.

Tres cifras que dicen si la migración está terminada

El registro original en Go contenía 23 plataformas y 329 comandos. Por su parte, el conjunto de comandos que Python genera a partir de sus declaraciones de argparse comparte exactamente 201 elementos con el registro antiguo. Quedan, por tanto, 128 comandos presentes solo en Go: ni se han migrado a Python ni se han dejado como stubs.

329 = 201 + 128. La resta no tiene ningún misterio técnico, pero es la única operación de toda la migración que responde a «¿está terminada?». Y no aparece cuando traduces funciones una a una. Un elemento ausente es un error por omisión: no lanza errores, no genera excepciones, no rompe tests; simplemente falta un nombre que debería existir. Nunca llegó al chat, así que puedes mirar doscientas marcas verdes sin verlo.

Por qué no conviene usar stubs ni proxies de compatibilidad

A mitad de una migración resulta tentador dejar marcadores para los comandos pendientes: un stub con raise NotImplementedError o un proxy de compatibilidad que redirija al binario antiguo para que «el catálogo de endpoints parezca completo». No lo hagas. Una carcasa vacía cuesta más que reconocer un hueco, por tres motivos.

Un stub invalida la reconciliación. El nombre del comando entra en el conjunto del lado nuevo, el diff pasa a ser 0 y parece que todo está hecho. Un hueco muestra honestamente un rojo; un stub es una mentira verde que transforma «quedan 128» en «están todos».

Un proxy de compatibilidad convierte una dependencia sin depurar en permanente. Si el proxy reenvía al binario antiguo de Go, ese runtime no podrá eliminarse nunca. La migración busca desprenderse del stack anterior, pero un proxy de reenvío permite que sobreviva indefinidamente bajo la etiqueta de «compatibilidad temporal».

Un endpoint a medio hacer también engaña a quien lo llama. Un Agent o una persona consulta el catálogo, da por hecho que funciona y se encuentra con runtime_unavailable; peor aún, podría recibir un éxito falso que devuelve silenciosamente un resultado vacío.

El hueco explícito es, en realidad, la opción más barata: el diff lo marca en rojo de inmediato y cualquiera puede ver cuánto falta. Es el mismo principio que el umbral de evidencia en ingeniería inversa de aplicaciones: marcar algo como «todavía no utilizable» siempre sale más barato que publicar una implementación a medio construir.

La estructura mínima del script de reconciliación

La regla central de la reconciliación cabe en una línea: los conjuntos de comandos de ambos lados deben derivarse de declaraciones, y nadie debe transcribirlos a mano. Si escribes manualmente una lista de «elementos migrados», introduces una tercera fuente de verdad que acabará divergiendo del código; en dos semanas será lo primero que falle.

En el lado nuevo, Python, la fuente única de verdad son las declaraciones de argparse en el cli.py de cada plataforma. Un módulo catalog recorre los subcomandos, exporta un conjunto {platform/command} y lo emite mediante python -m reverse describe --format json. El artículo sobre la interfaz como código explica por qué una declaración puede ser la fuente única de verdad y cómo derivar automáticamente el catálogo completo. En el lado antiguo, Go ya dispone de un mapa platform -> command, una allowlist inmutable compilada dentro del binario, así que exportar un JSON con la misma forma es trivial.

Con los dos archivos JSON preparados, el resto son operaciones de conjuntos:

# Both sides' command sets derive from declarations, not transcription.
# Transcribe by hand and you've added a third source of truth that will drift.
import json
from collections import Counter

def ids(path):
    doc = json.load(open(path))
    return {f"{p['name']}/{c['name']}"
            for p in doc["platforms"] for c in p["commands"]}

old = ids("go-registry.dump.json")      # old registry: immutable platform->command allowlist
new = ids("python-catalog.dump.json")   # python -m reverse describe --format json

missing = old - new     # old side only: each one needs a keep-or-drop verdict
added   = new - old     # new side only: new capability, logged separately
kept    = old & new     # intersection: migrated, but still check for semantic drift

assert missing | kept == old            # every old-side item classified, none dropped

by_platform = Counter(pc.split("/")[0] for pc in missing)  # goes straight into the README table

Esto se ejecuta en pocos milisegundos, no cuesta nada, es determinista y correcto al 100%. missing contiene esos 128 comandos y, agrupados por plataforma, quedan así:

Plataforma

Comandos sin migrar

xiaohongshu

33

tiktok

30

hotspot

21

douyin

19

reddit

8

weibo

7

bilibili

5

zhihu

3

linkedin

1

netease_music

1

Total

128

En este paso no hay lugar para un modelo.

Comparar listas con un modelo: caro, impreciso e irrepetible

Si prescindes del script, pegas ambas listas en un chat y preguntas «¿cuáles de los 329 no aparecen entre estos 201?», ocurren tres cosas, invariablemente.

Primero, el modelo omite elementos. Ante listas largas no calcula una diferencia de conjuntos elemento por elemento: se guía por una aproximación de «esto parece correcto», los elementos del final se diluyen y recibes una respuesta aparentemente completa a la que le faltan una docena. Después, inventa: informa como ausentes elementos presentes en ambos lados, o cuenta como migrados elementos que realmente faltan, porque imita el aspecto de un informe de reconciliación en vez de efectuar la diferencia. Por último, no es reproducible: repite la misma consulta con la misma entrada y la lista de ausencias cambia. Una reconciliación que arroja un resultado distinto cada vez no es una reconciliación.

Tampoco compensa por coste. El script tarda unos pocos milisegundos; pedir la comparación al modelo consume varios cientos de miles de tokens y exige varias rondas de autocorrección. Es más caro, más lento y menos fiable. Dejar la operación de conjuntos a la herramienta que hace operaciones de conjuntos es la afirmación menos polémica de este artículo.

El trabajo del modelo: explicar la diferencia, no decidir si se migró

El script te entrega 128 hechos de tipo «no se migró», pero un hecho no es una conclusión. Cada caso necesita una decisión de conservar o descartar, y toda decisión requiere una razón. Ahí es donde entra el modelo.

Debe explicar, uno por uno, por qué no se migró cada comando. ¿Es código muerto? ¿El endpoint upstream se retiró? ¿Se aplazó? ¿O, en el caso más delicado, no se eliminó sino que se integró en otro comando? Esa correspondencia oculta de «fusionado, no eliminado» no puede detectarse solo con la lista de ausencias: hay que leer ambos registros a la vez para identificarla.

Tampoco los 201 elementos de la intersección están a salvo. Haber migrado no garantiza que se haya mantenido la semántica: puede haber un comando con el mismo nombre pero un valor por defecto cambiado sin avisar, una semántica de paginación invertida o dos códigos de error reducidos a uno. Eso es deriva semántica, más sutil que un hueco porque el diff está en verde y el caso ni siquiera entra en missing. Detectarla exige que el modelo lea ambas implementaciones y valore si el comportamiento es equivalente; al final debe confirmarse mediante pruebas diferenciales, con comparación de fixtures en la tercera fase del flujo de trabajo de cuatro etapas. La capacidad de examinar una traducción aparentemente correcta y aun así decir «aquí cambió el comportamiento» es exactamente lo que aborda la sección sobre contraevidencia del artículo de fingerprinting. Un modelo débil se limitará a repetir «migración completada correctamente».

La división es sencilla: el script determina «¿está?», mientras que el razonamiento hace falta para decidir «¿debería conservarse?» y «¿ha cambiado?». En esta ocasión, tras revisarlos, 4 comandos que el diff marcaba en rojo resultaron necesarios y se restauraron como comandos nuevos de primera clase. El script decide el hecho, el modelo lo explica y la persona toma la decisión: tres capas, cada una en su sitio.

Qué modelo usar en cada fase

Las cuatro capas siguientes pertenecen a la explicación. La capa de decisión, el diff, no utiliza ningún modelo; esa es la diferencia fundamental entre este enfoque y otros artículos sobre «migraciones con IA».

Fase

Capacidad necesaria

Elección

model id

Leer ambos registros a la vez e identificar correspondencias de «no eliminado, sino fusionado en otro lugar»

Contexto largo para leer todas las declaraciones de ambos lados simultáneamente

Kimi K3

kimi-k3

Primera clasificación de los 128 elementos ausentes, con borrador estructurado de conservar o descartar

Económico, con cientos de llamadas y alta concurrencia

Claude Sonnet 5

claude-sonnet-5

Evaluar deriva semántica: está migrado, pero ¿cambió su comportamiento?

Razonamiento sólido y disposición para afirmar «esto cambió»

Claude Opus 5

claude-opus-5

El elemento migró, pero el fixture no coincide: explicar la diferencia por parámetros o estructura de respuesta

Atribución con razonamiento intermedio

GPT-5.6 Sol

gpt-5.6-sol

La tercera capa es la que más merece la pena probar. El juicio sobre deriva semántica mide exactamente si el modelo discutirá una traducción que ya parece satisfactoria, y es donde cambiar de modelo modifica más el resultado. El protocolo es este:

  1. Usa una migración real entre dos lenguajes de tu propio proyecto y ejecuta el script para obtener el conjunto missing; en esta fase no interviene ningún modelo.

  2. Etiqueta manualmente de 10 a 15 casos con una verdad de referencia: descartar, conservar, fusionado en otro lugar o aplazado.

  3. Envía el mismo prompt, «explica para cada elemento si se conserva o se descarta», a claude-opus-5 y a un modelo económico. Revisa dos cosas: si la justificación apunta a un hecho específico del código o entrega vaguedades como «posiblemente obsoleto», y cuántas correspondencias de elementos fusionados en otro lugar identifica cada uno.

  4. El número de correspondencias ocultas que detecte te indicará si te atreves a confiarle la primera pasada.

El problema no es elegir modelo: es el coste de cambiar entre ellos

Cuatro modelos de tres proveedores implican tres SDK, tres esquemas de autenticación y tres formatos de error. Reescribir el cliente tres veces para cambiar de nivel no merece la pena, por lo que la mayoría usa un único modelo de principio a fin. Cuando llega la revisión de deriva semántica, la que más necesita un modelo de razonamiento, recurren a una opción barata que solo ofrece respuestas vagas y dejan pasar toda la deriva verde.

AIReiter aplana esa capa: una clave, una interfaz compatible con OpenAI y los cuatro niveles detrás. Para cambiar de modelo basta con modificar el campo model del cuerpo de la petición.

# Semantic-drift review / per-item keep-or-drop: the reasoning tier
curl https://aireiter.com/api/v1/chat/completions \
  -H "Authorization: Bearer $AIREITER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5",
    "messages": [{"role": "user", "content": "<both implementations + this command migration status, ask if behavior is equivalent>"}]
  }'

# First pass on 128 missing items in bulk: change the model field, leave the rest
#   "model": "claude-sonnet-5"
# Diff attribution when a fixture won't match:
#   "model": "gpt-5.6-sol"

Si ya usas el SDK de OpenAI, apunta base_url a https://aireiter.com/api/v1 y no cambies nada más. Con el SDK de Anthropic, utiliza POST /api/v1/messages con la misma clave.

El precio encaja con este flujo: la primera pasada procesa cientos de elementos a la vez y se repite en cada ronda de migración, por lo que claude-sonnet-5, con alta concurrencia, es la opción más barata. La revisión de deriva semántica consiste en una docena de casos difíciles consultados una y otra vez con claude-opus-5, el más caro por elemento. Ambos son niveles de Claude, y el descuento del 30% se aplica justo a las partes más intensivas y costosas. gpt-5.6-sol se encarga de atribuir diferencias, con GPT a mitad de precio.

Conclusión

«Traducido» es una ilusión que puede crearse función a función. «Migrado» se resuelve con el diff. Las operaciones de conjuntos son trabajo del script, las explicaciones pertenecen al modelo y las decisiones corresponden a una persona. Ese orden no se puede alterar, y menos aún dejando que el modelo tome la decisión.

Hay un paso adicional que resulta especialmente fácil omitir: la lista de ausencias debe vivir en el README y mantenerse visible a largo plazo. El 128 debe seguir ahí hasta convertirse en 0 o hasta que cada elemento tenga escrito «no se migrará, porque X». Una reconciliación que solo existe en la conversación de una PR no es una reconciliación: la siguiente persona que se haga cargo no la verá y volverá a tropezar con los mismos 128 casos. Ese es el terreno común de este artículo, el texto sobre la interfaz como código y el artículo sobre por qué no construir un Model de respuesta unificado: deja que la fuente única de verdad hable por sí misma y no disperses las conclusiones entre los recuerdos de distintas personas.