venue_probe: atteso hardcoded False in diagnose, l'unico chiamante non cade mai nello slot (#35 B11)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 15:11:29 +00:00
parent 604fde689e
commit e82f2ee163
2 changed files with 45 additions and 54 deletions
+33 -22
View File
@@ -28,19 +28,21 @@ col codice 11051, e' manutenzione; se non risponde affatto, e' il venue ma per u
se la sonda stessa non parte, il verdetto e' IGNOTO — **«non vedo» non e' «va tutto bene»** (P5).
La sonda parte SOLO quando qualcosa e' gia' fallito: sul percorso sano non costa niente.
⚠️ **LA FINESTRA NON E' UNA PROVA.** `in_release_window()` dice soltanto che l'orologio e'
compatibile con lo slot di release. Non declassa niente da sola: declassa solo cio' che la SONDA
ha gia' riconosciuto come manutenzione, e solo dentro la durata ANNUNCIATA. Una manutenzione che
sfora resta rumorosa (il 18/08 Deribit annuncio' 15-30 minuti e resto' giu' oltre l'ora), e un
guasto vero di martedi' mattina resta rumoroso: altrimenti si sarebbe costruito il silenzio
proprio nell'ora in cui e' piu' probabile che serva. E' la stessa logica di
`venue_watch.MAINT_GRACE_HOURS` — *declassa dentro la tolleranza, RIALZA oltre* — applicata pero'
a una domanda diversa (**questo giro** e' spiegato?) e quindi alla risoluzione del giro orario,
non delle ore consecutive. Sono due politiche vicine e distinte: non accorparle senza misurare.
REGOLA P9 — *un allarme massimo speso per un evento atteso e' un allarme che non verra' letto il
giorno che e' vero.* E' il motivo per cui `atteso` esiste: declassa la GRAVITA', **non** registra
di meno. L'episodio finisce nel log e nella notifica comunque.
⚠️ **IL RAMO CHE DECLASSAVA E' MORTO, RIMOSSO DA `diagnose()` (#35 B11, 27/09/2026).**
`in_release_window()` diceva se l'orologio cadeva nello slot di release Deribit (martedi'
09:00-09:30 UTC); `diagnose()` la usava per declassare la GRAVITA' di una manutenzione gia'
riconosciuta dalla SONDA (REGOLA P9 — *un allarme massimo speso per un evento atteso e' un
allarme che non verra' letto il giorno che e' vero*). Ma l'UNICO chiamante di `diagnose()` e'
`book_execute.py`, in cron al minuto **:47** di ogni ora (dal 25/08): la finestra 09:00-09:30 non
contiene mai :47, quindi quel ramo non poteva MAI diventare vero — si leggeva come una
declassazione attiva e non lo e' mai stata. Il campo `atteso` RESTA sul verdetto
(`book_execute.py` lo scrive nel dict del report: cambiare il formato per una pulizia e' un costo
che non vale la pena) ma e' hardcoded a False in `diagnose()`. `in_release_window()` e le
costanti `RELEASE_*` restano invece nel modulo: le usa `test_book_cadenza.py`/`test_revisione.py`
per verificare che i cron di book/review evitino lo stesso slot — uno scopo indipendente da
`diagnose()` e ancora valido. Per declassare di nuovo un evento annunciato in `diagnose()` la
strada e' legarlo alla DURATA dell'annuncio (`venue_news`, se e quando esiste), non all'orologio
di un chiamante che non e' mai sincrono con lo slot.
"""
from __future__ import annotations
@@ -53,7 +55,7 @@ import requests
from src.live.venue_watch import is_maintenance # P1: la firma 11051 vive LI', non qui
# --- verdetti -----------------------------------------------------------------------------
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051: atteso se dentro lo slot
V_MANUTENZIONE = "VENUE_MANUTENZIONE" # il venue dichiara 11051
V_VENUE_GIU = "VENUE_GIU" # il venue non risponde, e non dice manutenzione
V_GATEWAY = "GATEWAY" # il venue sta bene -> il guasto e' NOSTRO (riparabile)
V_IGNOTO = "IGNOTO" # la sonda non e' partita: "non vedo" (P5)
@@ -70,8 +72,12 @@ PROBE_TIMEOUT = 8.0
# 30/06/2026 alle 09:00 UTC, "down for around 15-30 minutes").
# ⚠️ M10: *due punti non fanno una tendenza*. Qui i punti sono quattro e c'e' una meccanica
# dichiarata dal venue, ma resta una REGOLARITA' OSSERVATA, non un contratto: se Deribit sposta
# lo slot, questa costante mente in silenzio. E' per questo che la finestra non decide da sola —
# decide la sonda, e la finestra al massimo declassa.
# lo slot, questa costante mente in silenzio.
# ⚠️ #35 B11 (27/09/2026): questa finestra NON declassa piu' niente in `diagnose()` (vedi sotto):
# l'unico chiamante gira in cron al minuto :47, che non ci cade mai dentro. La funzione e le
# costanti restano perche' `test_book_cadenza.py`/`test_revisione.py` le usano per un controllo
# diverso e ancora valido: che i cron di `book`/`review` cadano FUORI da questo slot (P1, la
# stessa finestra, non una seconda dichiarata a mano).
RELEASE_WEEKDAY = 1 # lunedi'=0 -> 1 = martedi'
RELEASE_START_MIN = 9 * 60 # 09:00 UTC
RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione torna una notizia.
@@ -79,7 +85,8 @@ RELEASE_LEN_MIN = 30 # durata ANNUNCIATA. Oltre questa la manutenzione to
@dataclass(frozen=True)
class Diagnosi:
"""Il verdetto (*cosa*), la spiegazione (*perche'*), e se l'evento era atteso (P9)."""
"""Il verdetto (*cosa*) e la spiegazione (*perche'*). `atteso` resta nel formato ma e' sempre
False (#35 B11): l'unico chiamante non cade mai nella finestra che l'avrebbe declassato."""
verdetto: str
perche: str
atteso: bool
@@ -87,7 +94,8 @@ class Diagnosi:
@property
def gravita(self) -> str:
"""Emoji del titolo di notifica. `atteso` declassa, non silenzia (P9)."""
"""Emoji del titolo di notifica. `atteso` e' sempre False (#35 B11): ramo morto lasciato
per compatibilita' di formato, non per una declassazione che avviene ancora."""
if self.atteso:
return "ℹ️"
return {V_GATEWAY: "🛑", V_VENUE_GIU: "🛑", V_MANUTENZIONE: "⚠️"}.get(self.verdetto, "⚠️")
@@ -106,8 +114,9 @@ class Diagnosi:
def in_release_window(now: datetime | None = None) -> bool:
"""L'orologio e' dentro lo slot di release Deribit (martedi' 09:00-09:30 UTC)?
PURA e testabile. Non e' una prova di niente da sola: vedi la nota in testa al modulo.
"""
PURA e testabile. Non declassa piu' niente in `diagnose()` (#35 B11): resta per chi verifica
che un cron (book, review) sia schedulato fuori da questo slot (P1: la finestra si deriva da
qui, non si riscrive una seconda volta nel test)."""
now = now or datetime.now(timezone.utc)
now = now.astimezone(timezone.utc)
if now.weekday() != RELEASE_WEEKDAY:
@@ -136,7 +145,6 @@ def probe_public(timeout: float = PROBE_TIMEOUT) -> tuple[bool | None, str]:
def diagnose(errori_osservati: list[str] | None = None,
now: datetime | None = None,
sonda=None) -> Diagnosi:
"""Classifica un guasto gia' avvenuto. `sonda` e' iniettabile: i test non toccano la rete.
@@ -169,7 +177,10 @@ def diagnose(errori_osservati: list[str] | None = None,
verdetto = V_VENUE_GIU
perche = "l'API pubblica Deribit non serve, e non dichiara manutenzione"
atteso = verdetto == V_MANUTENZIONE and in_release_window(now)
# #35 B11: qui c'era `verdetto == V_MANUTENZIONE and in_release_window(now)`. L'unico
# chiamante (book_execute.py, cron :47) non cade mai nella finestra 09:00-09:30 di martedi':
# il ramo non poteva mai diventare vero. Il campo resta (formato del report), sempre False.
atteso = False
prova = "; ".join([*errori[:3], grezzo])[:500]
return Diagnosi(verdetto=verdetto, perche=perche, atteso=atteso, prova=prova)