Zeno sul campo: cosa ha insegnato il prodotto vero

Zeno dopo il lancio: createdAt non è live, i rami morti viaggiano inosservati, la prova di zero vuole una regola scritta. Tre lezioni dal campo.

In breve. Zeno, il mio coach AI da 5 minuti al giorno, è stato approvato sull'App Store il 13 aprile 2026 e si è dato i tetti di spesa tre giorni dopo. Poi è arrivato il campo vero, quello dopo il lancio: il 15 settembre 2026 due PR hanno cambiato come provo che il prodotto gira — createdAt non è live, un ramo morto viaggiava inosservato da sempre, e la prova di zero ora ha una regola scritta. Qui solo ciò che il prodotto ha insegnato dopo l'approvazione: la saga dello store sta in tre rifiuti dell'App Store, il meccanismo dei tetti in BudgetGuard di Zeno, i tre attriti da misurare in cosa tracciare davvero. La mappa dei tre episodi è in Zeno build in public.

createdAt non è live

Il 15 settembre 2026 ho smesso di fidarmi di un timestamp: Railway dichiara createdAt alla creazione dell'oggetto deployment, ma con l'healthcheck il traffico passa solo dopo che il controllo è verde, in un istante che nessuno dichiara. Usare createdAt come confine del live era una bugia comoda (ad39639).

Ora /health aggiunge un served_at assoluto per richiesta, con no-store, e release-evidence.py conia liveObservedAt da quell'osservazione: il confine del live è osservato, non dichiarato. createdAt resta solo come deploymentCreatedAt informativo, e un ipotetico campo deployedAt non viene mai rietichettato (ad39639, b4c7018).

L'identità della release è durevole: /health rispecchia RAILWAY_GIT_COMMIT_SHA e RAILWAY_DEPLOYMENT_ID di Railway in un oggetto build additivo, e release-evidence.py lega il deployment runtime esatto al suo evento SUCCESS su Railway in modo fail-closed, sola lettura (8163d7e). E lo scope va dichiarato dall'operatore — URL di health, service e environment espliciti, con uscita 3 prima di qualsiasi I/O se mancano: il contesto collegato della CLI non si considera mai affidabile (b4c7018).

I rami morti viaggiano inosservati

Sempre il 15 settembre 2026, il sospetto cade su un ramo specialista — route_to_specialist — che non ha chiamanti da quando è stato introdotto. Zero. Ha viaggiato nel prodotto senza che nessuno lo notasse, perché niente lo guardava (b49bad1).

Invece di rimuoverlo alla cieca, ho strumentato l'equivalente vivo della stessa famiglia di difetti: _determine_session_type sul percorso di avvio sessione (parse exact-match, default silenzioso su stress_management). Un evento limitato session_type_routing_decision per determinazione — INFO per le mappate, WARN per i fallback — solo enum, mai output grezzi, contenuti o identificativi. Il comportamento di selezione resta identico al byte: solo osservabilità (b49bad1). Uno script di proprietà del prodotto, routing-evidence.py, aggrega decisioni e fallback dai log di produzione in sola lettura, con la nota di maturità registrata in docs/routing-observability.md.

La prova di zero vuole una regola scritta

Il ramo morto e il candidato vivo condividono la famiglia ma non avranno mai la stessa identità canonica: distinzione congelata (40d5c79). E la prova di zero — dimostrare che i fallback non accadono — ora ha una regola scritta: almeno 15 decisioni, zero fallback, parsing completo. Denominatore fissato sugli avvii di sessione automatici. Test di gate dalla A alla I: separazione delle classi, completezza della prova di zero, ramo morto intoccato, denominatore, silenzio dei non-auto, distinguibilità tra mappate e fallback (40d5c79).

L'epoca di evidence è ancorata al deployment e filtrata sul server: routing-evidence.py ora richiede --deployment-id dall'output LIVE, passato posizionalmente con service e environment espliciti più un filtro server-side sulla famiglia di eventi — nessuna sorgente implicita o ultima può coniare evidence. I documenti LIVE riecheggiano il deploymentId fissato, il parser accetta solo tuple di eventi valide esatte, e i documenti fissano la sequenza di produzione a un'epoca sola con la regola di stop-on-replacement (a607207).

Fatti e limiti

FACT. Il 15 settembre 2026, su Zeno: createdAt di Railway non è più usato come confine del live; /health aggiunge served_at assoluto per richiesta e release-evidence.py conia liveObservedAt dall'osservazione (ad39639, 8163d7e).

FACT. Lo stesso giorno: /health rispecchia RAILWAY_GIT_COMMIT_SHA e RAILWAY_DEPLOYMENT_ID; lo scope di release richiede service e environment dichiarati, con uscita 3 se mancano (8163d7e, b4c7018).

FACT. Lo stesso giorno: route_to_specialist risulta senza chiamanti dall'introduzione; _determine_session_type viene strumentato con un evento limitato per determinazione, solo enum, comportamento identico al byte (b49bad1).

FACT. Lo stesso giorno: la prova di zero richiede almeno 15 decisioni, zero fallback e parsing completo, con denominatore sugli avvii automatici; l'evidence richiede --deployment-id fissato e filtro server-side (40d5c79, a607207).

INTERPRETATION. Il confine del live va osservato, non letto da un campo: è la mia regola da settembre 2026, nata da un timestamp che mentiva per costruzione, non da una statistica.

INTERPRETATION. Il codice morto si trova guardando, non ricordando: un ramo senza chiamanti vive finché qualcuno non conta i chiamanti. Strumentare prima di rimuovere è la disciplina che applico da qui in poi.

Limiti: un prodotto, un giorno di hardening, tutto solo osservabilità — il comportamento in produzione è identico al byte, niente è stato «riparato» il 15 settembre. Niente dati d'uso, retention o download: questo pezzo regge su release e routing osservati nei diff, non su risultati utenti. Nessun contenuto utente, nessun identificativo.

Domande frequenti

Perché createdAt non basta come prova del live?

Perché è la nascita dell'oggetto deployment, non il passaggio del traffico: con l'healthcheck il traffico passa dopo, in un istante non dichiarato. La prova è un served_at osservato per richiesta, non un campo letto da un'API.

Quando va rimosso un ramo senza chiamanti?

Dopo averlo contato e strumentato, non prima. route_to_specialist aveva zero chiamanti dall'introduzione: la rimozione cieca avrebbe chiuso un caso mai osservato. Prima l'evento sul percorso vivo, poi — con la prova — la rimozione.

Cosa serve per dire «zero fallback»?

Una regola scritta: almeno 15 decisioni, zero fallback, parsing completo, denominatore fissato. Senza denominatore e senza soglia, «zero» è un'impressione, non una prova.