AIREITER

329 comandi, 128 ancora assenti: una migrazione si riconcilia con la teoria degli insiemi, non con un modello

Ultimo Aggiornamento: 2026-07-31 07:49:15

Duecento funzioni tradotte, test tutti verdi, codice pulito e idiomatico: è facile dichiarare chiusa una migrazione. Ma il conteggio reale può raccontare un'altra storia. In una migrazione da Go a Python, su 329 comandi dell'implementazione originale, 128 non erano ancora presenti sul lato nuovo.

Qui sta la confusione fra "tradotto" e "migrato". Verificare che una singola funzione sia stata tradotta correttamente è uno dei compiti in cui un modello riesce bene. Stabilire invece se l'intero sistema è stato migrato è una domanda di natura diversa: è un'operazione sugli insiemi. Ed è proprio il genere di operazione che un modello non dovrebbe gestire, perché può facilmente dare un'impressione fuorviante di completezza.

Ecco il bilancio di una migrazione reale da Go a Python e il motivo per cui va chiuso con uno script, lasciando al modello il solo compito di spiegare ciò che emerge.

Tre numeri che dicono a che punto è davvero la migrazione

Il registry Go originale conteneva 23 piattaforme e 329 comandi. Il set di comandi che il nuovo lato Python ricava dalle dichiarazioni argparse ha un'intersezione esatta di 201 elementi con il registry precedente. Restano quindi 128 comandi presenti soltanto sul lato Go: non sono stati migrati in Python e non sono nemmeno rimasti come stub.

329 = 201 + 128. È una sottrazione senza alcun contenuto tecnico, eppure è l'unica cosa nell'intera migrazione che risponde alla domanda "è finita?". Traducendo una funzione alla volta, quel dato non compare mai. Un elemento mancante è un errore per assenza: non genera errori, eccezioni o test falliti; semplicemente, un nome che dovrebbe esistere non c'è. Non è mai entrato nella chat e duecento spunte verdi non lo renderanno visibile.

Perché evitare stub e proxy di compatibilità

A metà migrazione viene naturale lasciare un segnaposto per i comandi ancora da fare: uno stub con raise NotImplementedError, oppure un proxy di compatibilità che inoltra al vecchio binario, così da far sembrare completo il catalogo degli endpoint. Meglio non farlo. Un guscio vuoto costa più di una lacuna, per tre motivi.

Prima di tutto, uno stub falsifica la riconciliazione. Il nome del comando entra nel set del nuovo lato, il diff scende a 0 e sembra che il lavoro sia concluso. Una lacuna è un rosso onesto; uno stub è una bugia verde che trasforma "ne restano 128" in "ci sono tutti".

Un proxy di compatibilità, poi, cristallizza una dipendenza che si voleva eliminare. Se inoltra al vecchio binario Go, il runtime Go non potrà mai essere rimosso. Lo scopo della migrazione è abbandonare il vecchio stack; il proxy, sotto l'etichetta della "compatibilità temporanea", gli permette di restare per sempre.

Infine, un endpoint costruito a metà inganna chi lo invoca. Un Agent o una persona consulta il catalogo, presume che funzioni, lo chiama e riceve runtime_unavailable. Nel caso peggiore ottiene un falso successo, con un risultato vuoto restituito in silenzio.

La lacuna dichiarata è in realtà l'opzione più economica: il diff la evidenzia subito in rosso e tutti vedono quanto lavoro resta. Vale lo stesso principio della soglia delle evidenze nel reverse engineering delle app: indicare apertamente che qualcosa "non è ancora utilizzabile" costa sempre meno che pubblicarne una versione incompleta.

Lo scheletro dello script di riconciliazione

Il cuore della riconciliazione è semplice: entrambi i set di comandi devono essere derivati dalle dichiarazioni, senza trascrizioni manuali. Scrivere a mano una lista dei comandi "migrati" significa introdurre una terza fonte di verità, destinata a divergere dal codice; nel giro di due settimane sarà la prima cosa a rompersi.

Nel nuovo lato Python, l'unica fonte di verità sono le dichiarazioni argparse nei file cli.py di ogni piattaforma. Un modulo catalog percorre i sottocomandi ed esporta un set {platform/command}, prodotto da python -m reverse describe --format json. Il motivo per cui una dichiarazione può essere l'unica fonte di verità, e il modo in cui il catalogo viene derivato automaticamente, sono al centro dell'articolo sull'interfaccia come codice. Sul lato Go esiste già una mappa platform -> command, una allowlist immutabile compilata nel binario: esportare JSON con la stessa struttura è banale.

Una volta disponibili i due file JSON, il resto è pura teoria degli insiemi:

# 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

Lo script gira in pochi millisecondi, non ha costi, è deterministico ed è corretto al 100%. missing contiene quei 128 comandi; aggregati per piattaforma, danno questa tabella:

Piattaforma

Comandi non migrati

xiaohongshu

33

tiktok

30

hotspot

21

douyin

19

reddit

8

weibo

7

bilibili

5

zhihu

3

linkedin

1

netease_music

1

Totale

128

In questo passaggio non c'è alcun ruolo per un modello.

Il confronto riga per riga affidato a un modello: costoso e inaffidabile

Se si salta lo script, si incollano le due liste in chat e si chiede "quali dei 329 non compaiono in questi 201?", accadono puntualmente tre cose.

Il modello omette elementi: quando la lista è lunga, non calcola una differenza tra insiemi elemento per elemento, ma procede per approssimazione; gli elementi in coda si diluiscono e il risultato sembra completo, pur mancando magari una dozzina di voci. Inventa elementi: segnala come assenti comandi presenti su entrambi i lati oppure considera migrati comandi che non lo sono, perché sta imitando la forma di un report di riconciliazione anziché calcolare la differenza. E non è riproducibile: con lo stesso input, due richieste possono restituire liste diverse. Una riconciliazione che cambia risultato ogni volta non è una riconciliazione.

Non conviene nemmeno sul piano dei costi: lo script impiega pochi millisecondi; chiedere al modello di confrontare le liste richiede qualche centinaio di migliaia di token e più giri di autoverifica. È più lento, più costoso e meno affidabile. Affidare le operazioni sugli insiemi allo strumento che sa fare operazioni sugli insiemi è probabilmente l'affermazione meno controversa di questo articolo.

Il ruolo giusto del modello: spiegare il diff, non stabilire se qualcosa è migrato

Lo script consegna 128 fatti del tipo "non migrato", ma un fatto non è una conclusione. Ogni elemento richiede una decisione: mantenerlo oppure eliminarlo. E una decisione richiede una motivazione. Questo è il terreno del modello.

Bisogna spiegare, uno per uno, perché non è stato migrato. È codice morto? L'endpoint upstream è stato ritirato? Il lavoro è stato rinviato? Oppure, nel caso più insidioso, la funzionalità non è stata cancellata ma incorporata in un altro comando: il nome è sparito, ma la capacità esiste ancora. Questa corrispondenza nascosta, "unito e non eliminato", non emerge dalla sola lista dei mancanti: bisogna leggere contemporaneamente entrambi i registry per individuarla.

Nemmeno i 201 elementi nell'intersezione sono automaticamente al sicuro. Migrato non significa che la semantica sia rimasta invariata: un comando con lo stesso nome può avere un valore predefinito modificato senza evidenza, una semantica di paginazione invertita, due codici di errore confluiti in uno. È semantic drift, più subdolo di una lacuna perché il diff è verde e l'elemento non entra mai in missing. Per rilevarlo, il modello deve leggere entrambe le implementazioni e valutare se il comportamento sia equivalente; la conferma finale arriva dal differential testing, cioè dal confronto delle fixture nella terza fase del workflow in quattro stadi. Saper guardare una traduzione apparentemente riuscita e dire comunque "qui il comportamento è cambiato" è esattamente ciò di cui parla la sezione sulle controevidenze nell'articolo sul fingerprinting; un modello debole si limita a ripetere "migrazione completata con successo".

La divisione dei compiti è quindi netta: allo script spetta decidere se un elemento c'è; al modello capire se debba rimanere e se sia cambiato; all'umano prendere la decisione. In questo caso, 4 comandi segnalati in rosso dal diff si sono rivelati necessari dopo la revisione e sono stati ripristinati come nuovi comandi di prima classe. Script per stabilire, modello per spiegare, umano per decidere: tre livelli, ognuno al proprio posto.

Il modello giusto per ogni fase

Tutti e quattro i livelli qui sotto appartengono al livello di spiegazione. Il livello decisionale, cioè il diff, non usa alcun modello: è questa la linea di demarcazione rispetto ad altri articoli sulla "migrazione con AI".

Fase

Capacità richiesta

Scelta

model id

Fornire entrambi i registry insieme e individuare le corrispondenze "non eliminato, ma unito altrove"

Contesto lungo, lettura simultanea delle dichiarazioni complete di entrambi i lati

Kimi K3

kimi-k3

Prima valutazione keep-or-drop dei 128 elementi mancanti, bozza strutturata

Economico, centinaia di chiamate con alta concorrenza

Claude Sonnet 5

claude-sonnet-5

Valutazione del semantic drift: il comando è migrato, ma il comportamento è cambiato?

Ragionamento solido, disponibilità a dire "qui è cambiato"

Claude Opus 5

claude-opus-5

Comando migrato, ma fixture non corrispondente: spiegazione rispetto a parametri o struttura della risposta

Attribuzione con ragionamento intermedio

GPT-5.6 Sol

gpt-5.6-sol

Il livello più utile da testare è il terzo. La valutazione del semantic drift misura esattamente la capacità di contestare una traduzione già considerata riuscita, ed è qui che cambiare modello modifica maggiormente il risultato. Il protocollo:

  1. Prendi una tua migrazione reale fra due linguaggi e genera con lo script il set missing, senza usare modelli in questo passaggio.

  2. Etichetta manualmente da 10 a 15 elementi con una verità di riferimento: eliminare, mantenere, unito altrove, rinviato.

  3. Invia lo stesso prompt, "spiega keep-or-drop per ogni elemento", a claude-opus-5 e a un modello di fascia economica. Valuta due aspetti: la motivazione punta a un fatto concreto nel codice oppure restituisce vaghezze come "forse deprecato"? E quante corrispondenze unito-altrove individua ciascun modello?

  4. Il numero di corrispondenze nascoste rilevate è il criterio per decidere se affidargli o meno la prima valutazione.

Il vero attrito non è scegliere il modello, ma passare da uno all'altro

Quattro modelli di tre fornitori, tre SDK, tre schemi di autenticazione, tre formati di errore. Riscrivere il client tre volte per cambiare livello non conviene, quindi la maggior parte dei team usa un solo modello dall'inizio alla fine. Quando arriva la revisione del semantic drift, quella che richiede davvero un modello capace di ragionare, finisce per usare una fascia economica che produce solo vaghezze e lascia passare indisturbato tutto il drift verde.

AIReiter appiattisce questo livello: una chiave, un'interfaccia compatibile con OpenAI, tutti e quattro i livelli dietro le quinte. Per cambiare modello basta modificare il campo model nel body della richiesta.

# 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"

Se usi già l'SDK OpenAI, imposta base_url su https://aireiter.com/api/v1 e non cambiare altro. Con l'SDK Anthropic, usa POST /api/v1/messages con la stessa chiave.

Anche il prezzo si adatta a questo flusso: la prima valutazione elabora centinaia di elementi insieme e viene rieseguita a ogni ciclo della migrazione, quindi claude-sonnet-5 ad alta concorrenza è l'opzione più economica; la revisione del semantic drift affronta ripetutamente una dozzina di casi difficili con claude-opus-5, ed è la fase più costosa per elemento. Entrambi sono livelli Claude, e lo sconto del 30% si applica proprio alle parti più dense e più costose. gpt-5.6-sol gestisce l'attribuzione del diff, con GPT a metà prezzo.

Conclusioni

"Tradotto" è un'illusione costruita su una singola funzione. "Migrato" lo stabilisce il diff. Le operazioni sugli insiemi vanno allo script, le spiegazioni al modello, le decisioni all'umano. L'ordine non va invertito, e soprattutto non bisogna lasciare che sia il modello a decidere.

C'è un ultimo passaggio che si tende a saltare: la lista dei mancanti deve finire nel README e restare visibile nel tempo. Quel 128 rimane lì finché non diventa 0, oppure finché ciascun elemento non riporta per iscritto "non verrà migrato, perché X". Una riconciliazione confinata alla discussione di una PR non è una riconciliazione: chi subentra dopo non la vede e rischia di ripartire dagli stessi 128 problemi. È il terreno comune di questo articolo, dell'articolo sull'interfaccia come codice e dell'articolo sul perché non costruire un Model di risposta unificato: lasciare che l'unica fonte di verità parli da sé, senza disperdere le conclusioni nella memoria delle persone.