revisione settimanale in sola lettura: revisore fable, rapporto firmato + Telegram, lunedi' 06:15 UTC
- src/live/revisione.py: materiale (CLAUDE.md, config, crontab, git, monitor_health, stati dei sorveglianti, 7 g di giornale e diari, ~195k caratteri), prompt con dieci regole (sola lettura, fonte per ogni segnalazione, solo numeri del materiale, §3 non si ripropone, idee nuove solo citando la memoria, gate con data e criterio), guardia sui numeri, rapporto firmato (P13), Telegram = Sintesi con taglio dichiarato; modello muto o risposta senza titoli -> rapporto «NON eseguita» + 🚨 (P5). - scripts/live/revisione.py (guardia cli.valida, --secco/--no-telegram/--quiet/--giorno/--modello), scripts/cron_review.sh, crontab `15 6 * * 1`. - primo giro reale: il prompt su argv moriva con `Argument list too long` senza rapporto ne' allerta -> prompt su stdin, ogni eccezione diventa errore dichiarato; `analista.pulisci` toglieva i titoli. Secondo giro: docs/revisioni/2026-09-09.md, stato sospetta (cifre in formato italiano), nessun URGENTE. - scartati con la memoria: autoregolazione dei parametri, generazione automatica di strategie, macro da internet (diario 2026-09-09c, memoria 40). - test: tests/test_revisione.py (18) + 2 in test_cli_flag; suite 1031/1031. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
This commit is contained in:
@@ -113,6 +113,16 @@ deposito va **una riga di giornale**, non ricostruita dopo. `r0901d_soldi_fermi.
|
|||||||
|
|
||||||
**Candidati in forward-monitor** (nessuno nel book): XSR01 · DVOLSPREAD · STATARB · PREVDAY.
|
**Candidati in forward-monitor** (nessuno nel book): XSR01 · DVOLSPREAD · STATARB · PREVDAY.
|
||||||
|
|
||||||
|
📌 **Revisione SETTIMANALE in sola lettura (dal 2026-09-09).** Lunedi' 06:15 UTC `scripts/cron_review.sh` →
|
||||||
|
`scripts/live/revisione.py` → `src/live/revisione.py`: un revisore **diverso dall'autore** (`claude-fable-5-1`)
|
||||||
|
rilegge CLAUDE.md, config, crontab, commit, `monitor_health`, 7 giorni di giornale e di diari, stato dei sorveglianti
|
||||||
|
(~195k caratteri), e scrive `docs/revisioni/<data>.md` **firmato** + la Sintesi su Telegram. **Propone, non esegue:**
|
||||||
|
l'unico file che scrive e' il rapporto; le decisioni vincolanti (§3) non le ripropone; un'idea «nuova» la propone solo
|
||||||
|
citando la memoria che ha ucciso la simile. Il rapporto e' opinione di un lettore fallibile (P13): guardia sui numeri
|
||||||
|
come `analista`. Un modello muto produce un rapporto che lo dice, e un 🚨 (P5). Deciso dall'operatore il 09/09 sera
|
||||||
|
(«fai») dopo aver **scartato** autoregolazione dei parametri e generazione automatica di strategie: la ricerca non e'
|
||||||
|
il vincolo binding dal 26/07, e un generatore e' una grid search senza fondo (M3).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. I numeri da citare — e quelli da NON citare
|
## 2. I numeri da citare — e quelli da NON citare
|
||||||
@@ -645,7 +655,8 @@ src/backtest/harness.py → harness ONESTO (load BTC/ETH, backtest_signals
|
|||||||
src/live/ → book.py (esecutore netto TP01+SKH01), shadow.py, deribit.py,
|
src/live/ → book.py (esecutore netto TP01+SKH01), shadow.py, deribit.py,
|
||||||
livefeed.py, venue_watch.py, monitor_health.py, notifier.py,
|
livefeed.py, venue_watch.py, monitor_health.py, notifier.py,
|
||||||
scale_watch.py (chiave di scala: 3 domande, 3 stati),
|
scale_watch.py (chiave di scala: 3 domande, 3 stati),
|
||||||
tradesdb.py · journal.py · analista.py (libro di bordo)
|
tradesdb.py · journal.py · analista.py (libro di bordo),
|
||||||
|
revisione.py (revisione settimanale, sola lettura, revisore fable)
|
||||||
src/data/eq_splits.py, eq_crosscheck.py → guardie sul feed equity
|
src/data/eq_splits.py, eq_crosscheck.py → guardie sul feed equity
|
||||||
scripts/research/ → ricerca (r<data>_*.py). Harness condiviso: alt/altlib.py
|
scripts/research/ → ricerca (r<data>_*.py). Harness condiviso: alt/altlib.py
|
||||||
scripts/analysis/ → SOLO tool sui dati certificati: rebuild_history, certify_feed,
|
scripts/analysis/ → SOLO tool sui dati certificati: rebuild_history, certify_feed,
|
||||||
@@ -653,11 +664,12 @@ scripts/analysis/ → SOLO tool sui dati certificati: rebuild_history
|
|||||||
scripts/live/ → book_execute · venue_watch · fee_watch · edge_watch ·
|
scripts/live/ → book_execute · venue_watch · fee_watch · edge_watch ·
|
||||||
monitor_health · collect_chain · paper_* · trades_db ·
|
monitor_health · collect_chain · paper_* · trades_db ·
|
||||||
journal · analista
|
journal · analista
|
||||||
scripts/cron_{book,daily,chain,opt_snapshot,vol_term}.sh
|
scripts/cron_{book,daily,chain,opt_snapshot,vol_term,review}.sh
|
||||||
docs/memory/ → LA MEMORIA (indice in testa a questo file)
|
docs/memory/ → LA MEMORIA (indice in testa a questo file)
|
||||||
docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key
|
docs/research/ → BRIEF-0822 · RESULTS-0822 (§1-77) · SPEC-scale-key
|
||||||
docs/diary/YYYY-MM-DD.md → una voce per esperimento (146)
|
docs/diary/YYYY-MM-DD.md → una voce per esperimento (146)
|
||||||
docs/journal/YYYY-MM-DD.md → libro di bordo, una voce al giorno, 4 livelli per provenienza
|
docs/journal/YYYY-MM-DD.md → libro di bordo, una voce al giorno, 4 livelli per provenienza
|
||||||
|
docs/revisioni/YYYY-MM-DD.md → revisione settimanale firmata (lunedi'), opinione del revisore
|
||||||
data/live/trades.db → i trade allineati col tempo (dentro il perimetro di backup)
|
data/live/trades.db → i trade allineati col tempo (dentro il perimetro di backup)
|
||||||
Old/ → ARCHIVIO pre-reset
|
Old/ → ARCHIVIO pre-reset
|
||||||
VERSION → semver
|
VERSION → semver
|
||||||
@@ -676,7 +688,8 @@ uv run python scripts/live/<script>.py --help # uso e flag (d
|
|||||||
uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill
|
uv run python scripts/live/trades_db.py --reconcile # incrocio delle 3 fonti sui fill
|
||||||
uv run python scripts/live/journal.py # voce del giorno (numeri + lettura)
|
uv run python scripts/live/journal.py # voce del giorno (numeri + lettura)
|
||||||
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
uv run python scripts/live/analista.py --secco # analisi del giorno, senza salvare
|
||||||
uv run pytest # test (1011, tutti verdi al 09/09 sera)
|
uv run python scripts/live/revisione.py --secco # taglia del materiale della revisione settimanale (niente modello)
|
||||||
|
uv run pytest # test (1031, tutti verdi al 09/09 sera)
|
||||||
```
|
```
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# 2026-09-09c — Revisione settimanale in sola lettura: cosa si costruisce e cosa no
|
||||||
|
|
||||||
|
**Richiesta dell'operatore:** *«schedulare ogni x tempo un processo di auto revisione dei dati e della
|
||||||
|
configurazione del sistema in modo che migliori con il tempo … autoregolazione e autoaggiornamento dei
|
||||||
|
sistemi o anche creare nuovi sistemi … valutando dati macroeconomici … e poi informa me tramite
|
||||||
|
Telegram»*. Dopo la discussione: *«fai»*.
|
||||||
|
|
||||||
|
**Esito in una riga:** costruita la **rilettura settimanale in sola lettura** con revisore diverso
|
||||||
|
dall'autore (`claude-fable-5-1`), rapporto firmato in `docs/revisioni/` e Sintesi su Telegram, lunedi'
|
||||||
|
06:15 UTC; **scartati** — con la memoria del progetto, non per prudenza generica — autoregolazione dei
|
||||||
|
parametri, generazione automatica di strategie e macro da internet. Libro, config, pesi: invariati.
|
||||||
|
|
||||||
|
## 1. Perche' tre pezzi su quattro sono no
|
||||||
|
|
||||||
|
| pezzo | cosa dice la memoria | esito |
|
||||||
|
|---|---|---|
|
||||||
|
| auto-revisione periodica | nove sorveglianti, ognuno sulla sua grandezza; nessuno rilegge l'insieme; la revisione col secondo modello ha trovato oggi due conclusioni false e un look-ahead | **si'** |
|
||||||
|
| autoregolazione dei parametri | TP01/SKH01 congelati (M20), 75/25 confermato 3 volte, `weights_tilt_null` fallisce; `scale_watch` esiste per rendere visibile ogni cambio di config; A8: una riga di config + una di giornale, decisione dell'operatore | **no** |
|
||||||
|
| generazione e test di strategie nuove | 69 filoni in 6 ondate; ogni candidato e' un trial (M3) e deflaziona tutto; la ricerca non e' il vincolo binding dal 26/07 (+0,046 €/g il miglior lead contro +4,07%/anno per €100/mese) | **no, salvo freno**: prima si apre la memoria e si batte il motivo della morte; i candidati vanno in forward-monitor con gate e data, mai nel book |
|
||||||
|
| dati macro da internet | gate macro / DVOL direzionale / skew de-risk: HEDGE o ridondanti (TP01 gia' flat nei crolli); un dato dal web non e' certificato (D1-D4; la APR USDC 3,40% pubblicata e mai incassata) | **no** |
|
||||||
|
|
||||||
|
Il centro dell'argomento: **il progetto ha gia' misurato che piu' ricerca non sposta il traguardo, i
|
||||||
|
bonifici si'**. Un ciclo automatico che cerca strategie ottimizza la leva sbagliata e paga in trial bruciati.
|
||||||
|
|
||||||
|
## 2. Cosa gira
|
||||||
|
|
||||||
|
`scripts/cron_review.sh` (lunedi' **06:15 UTC**: fuori dal :00, fuori dal martedi' 09:00, dopo il
|
||||||
|
`cron_daily` delle 00:30 cosi' il giornale di domenica e' chiuso) → `scripts/live/revisione.py --quiet`
|
||||||
|
→ `src/live/revisione.scrivi`. Riga installata in crontab (`15 6 * * 1`), letta dal test.
|
||||||
|
|
||||||
|
**Materiale** (sola lettura, ~195k caratteri, 27 voci): CLAUDE.md intero · `config/live.json` ·
|
||||||
|
`crontab -l` · `git log` 14 g + `git status` · `monitor_health` · stato dei sorveglianti (scale, vrp_f,
|
||||||
|
usde, balance, venue_news, movimenti dichiarati, watermark) · 7 pagine di giornale · diari della
|
||||||
|
settimana. Gli stati stanno **prima** delle voci grandi: alla prima stesura il tetto totale (320k) aveva
|
||||||
|
tagliato `usde_convert.jsonl` a 2 caratteri — un sorvegliante invisibile per un limite di lunghezza.
|
||||||
|
|
||||||
|
**Prompt**: dieci regole vincolanti (sola lettura · ogni segnalazione cita la fonte · solo numeri del
|
||||||
|
materiale · le decisioni di §3 non si ripropongono se la colonna «cosa la riapre» non e' soddisfatta ·
|
||||||
|
un'idea nuova solo citando la memoria che ha ucciso la simile · i gate con data e criterio, senza
|
||||||
|
anticipare · misurato ≠ dedotto · niente previsioni · URGENTE per cio' che serve all'operatore subito ·
|
||||||
|
cinque titoli fissi, ~1.000 parole). L'ultimo titolo, «Cosa ho letto e cosa mi mancava», e' l'elenco da
|
||||||
|
cui il materiale della settimana dopo migliora: e' l'unico «miglioramento col tempo» che questo sistema
|
||||||
|
si concede, e passa da una modifica al codice, cioe' da una revisione.
|
||||||
|
|
||||||
|
**Uscita**: `docs/revisioni/<data>.md` firmato (modello, ora, «SOLA LETTURA», «lettore fallibile P13»,
|
||||||
|
guardia sui numeri come `analista` con soglia 12, tabella del materiale letto). Telegram: la sola
|
||||||
|
Sintesi, taglio dichiarato sotto i 4.096. Modello muto o risposta senza i titoli → rapporto «NON
|
||||||
|
eseguita» col motivo e 🚨: il silenzio non e' una revisione (P5).
|
||||||
|
|
||||||
|
**Cosa non puo' fare, per costruzione**: scrivere altro che il rapporto (test sull'albero dei file:
|
||||||
|
`--secco` non scrive niente, un giro scrive UN file), eseguire proposte, cambiare config. Il modello e'
|
||||||
|
`claude-fable-5-1` per costante, e un test verifica che sia diverso da `analista.MODELLO_DEFAULT`.
|
||||||
|
|
||||||
|
## 3. Test — `tests/test_revisione.py` (16)
|
||||||
|
|
||||||
|
Finestra del materiale e file assenti dichiarati · ordine stati-prima-delle-voci-grandi · troncamento
|
||||||
|
dichiarato · regole nel prompt · guardia (vuoto, titoli mancanti, numeri fuori con controllo positivo) ·
|
||||||
|
rapporto firmato · modello muto → rapporto + 🚨 · Telegram con HTML neutralizzato e taglio dichiarato ·
|
||||||
|
`--secco` non chiama e non scrive · un giro scrive un solo file · `--no-telegram` · revisore ≠ autore ·
|
||||||
|
cadenza: il `.sh` dichiara «lunedi' HH:MM UTC», minuto ≠ 0, fuori dallo slot di release, e la crontab
|
||||||
|
installata ha gli stessi cinque campi (skip se illeggibile). `test_cli_flag` ha preso lo script da solo
|
||||||
|
(guardia `valida`, USO coi flag, flag del cron accettati).
|
||||||
|
|
||||||
|
## 4. Errori in sessione
|
||||||
|
|
||||||
|
- `_tronca(testo, n=MAX_CHARS_VOCE)`: il default e' catturato alla definizione, il test che patchava la
|
||||||
|
costante non vedeva niente — la stessa trappola del debito 12 (`connect`). Letto a runtime.
|
||||||
|
- Il tetto totale tagliava in coda: i sorveglianti erano gli ultimi e i primi a sparire. Riordinato, e
|
||||||
|
il test lo presidia.
|
||||||
|
|
||||||
|
## 5. Primo giro reale (22:03Z) — e i tre difetti che solo il giro reale poteva trovare
|
||||||
|
|
||||||
|
Il primo lancio e' **morto prima di chiamare il modello**: `OSError: Argument list too long` — 195k
|
||||||
|
caratteri di prompt passati come argomento alla CLI superano il limite del sistema, e `analista.interroga`
|
||||||
|
non lo catturava: zero rapporto, zero Telegram, **il silenzio che il modulo esiste per vietare**. Nessun
|
||||||
|
test l'avrebbe trovato (i test iniettano `interroga`): e' un difetto di trasporto, e il trasporto si valida
|
||||||
|
sul trasporto (P2). Tre correzioni: (1) `revisione.interroga` proprio, prompt su **stdin**, ogni eccezione
|
||||||
|
restituita come errore; (2) `scrivi` incapsula chiamata e invio: un'eccezione diventa un rapporto «NON
|
||||||
|
eseguita» + 🚨; (3) `_cmd` (crontab, git) dichiara qualunque eccezione invece di propagarla. Test con
|
||||||
|
controllo positivo su `OSError(7)`. Terzo difetto, trovato dal test: `analista.pulisci` **toglie le righe
|
||||||
|
che iniziano con `#`**, cioe' i cinque titoli che la guardia richiede — usarlo avrebbe reso ogni revisione
|
||||||
|
«rifiutata: titoli mancanti». Sostituito con `strip()`.
|
||||||
|
|
||||||
|
Secondo lancio: **rapporto scritto, stato `sospetta`**, 1.435 parole, `docs/revisioni/2026-09-09.md`.
|
||||||
|
La guardia ha segnato 11 numeri «non nel materiale»: sono cifre in **formato italiano** («4.477,46»,
|
||||||
|
«1.341») contro il materiale in formato USA («$4,477.46») — la normalizzazione di `analista` conosce una
|
||||||
|
convenzione sola. E' un limite dichiarato, non un errore del revisore: si aggiusta quando si vede quanto
|
||||||
|
spesso scatta. Contenuto: **nessun URGENTE**; tre verifiche in cima (una possibile attribuzione a reward di
|
||||||
|
400 USDE che erano un acquisto in `usde_watch` del 07/09 — `n_trades` fermo a 50 contro 54 ordini nel
|
||||||
|
registro; il cuscino USDC del debito §5.17 senza definizione balance/equity; il versamento del 04/09 non
|
||||||
|
dichiarato in `movimenti_dichiarati.jsonl`); una contraddizione in CLAUDE.md §1 sull'haircut (M28); la data
|
||||||
|
di arming che differisce fra §0, config e TWR; quattro sorveglianti senza timestamp o fuori da
|
||||||
|
`monitor_health`; e, come richiesto dalla regola 5, **zero idee di strategia**. «Cosa mi mancava»: stati
|
||||||
|
con ora di `edge/fee/venue/scale_watch`, l'output di `usde_watch.rendimento()`, i paper a oggi,
|
||||||
|
`cron_daily.sh`, il registro fill della settimana — l'elenco da cui il materiale della prossima settimana
|
||||||
|
migliora. Le segnalazioni sono dell'operatore e della prossima sessione: **nessuna e' stata applicata qui**
|
||||||
|
(sarebbe l'agente che esegue le proposte del revisore nello stesso giro, cioe' il ciclo che si e' scartato).
|
||||||
|
Costo: una chiamata, ~4 minuti.
|
||||||
@@ -1011,3 +1011,39 @@ impedire.** Danno reale quel giorno: nessuno — alle 09:47 l'equity era leggibi
|
|||||||
che si accende se la fixture viene rimossa.
|
che si accende se la fixture viene rimossa.
|
||||||
- ⚠️ **Resta il principio più largo:** oggi è deviato **solo** il watermark. `data/live/trades.db` e
|
- ⚠️ **Resta il principio più largo:** oggi è deviato **solo** il watermark. `data/live/trades.db` e
|
||||||
`data/live/book_executions.jsonl` sono esposti allo stesso errore (§5.12).
|
`data/live/book_executions.jsonl` sono esposti allo stesso errore (§5.12).
|
||||||
|
|
||||||
|
### Revisione settimanale in sola lettura (2026-09-09)
|
||||||
|
|
||||||
|
L'operatore ha chiesto «un processo di auto-revisione dei dati e della configurazione, che si autoregola,
|
||||||
|
si autoaggiorna, crea sistemi nuovi valutando mercato e dati macro, e mi informa su Telegram». La risposta,
|
||||||
|
misurata contro la memoria, e' stata in quattro pezzi: **la rilettura periodica manca e vale** (i nove
|
||||||
|
sorveglianti guardano ognuno la sua grandezza; nessuno rilegge il sistema intero, e la revisione con un
|
||||||
|
secondo modello ha trovato in una sessione due conclusioni false e un look-ahead); **l'autoregolazione dei
|
||||||
|
parametri no** (e' cio' che `scale_watch`/`venue_watch` trattano come anomalia; A8 della chiave di scala:
|
||||||
|
una riga di config, una di giornale, decisione dell'operatore); **la generazione automatica di strategie
|
||||||
|
solo con un freno** (ogni candidato e' un trial, M3, e la ricerca non e' il vincolo binding dal 26/07: il
|
||||||
|
miglior lead vale +0,046 €/giorno contro +4,07%/anno di drift per €100/mese in piu'); **il macro da
|
||||||
|
internet no** (gate macro, DVOL direzionale e skew de-risk sono gia' HEDGE/ridondanti; un dato dal web
|
||||||
|
non e' certificato — la APR USDC «pubblicata» e mai incassata). L'operatore ha scelto la prima («fai»).
|
||||||
|
|
||||||
|
Cosa gira: `scripts/cron_review.sh` (lunedi' 06:15 UTC, fuori dal :00 e dal martedi' 09:00, dopo il
|
||||||
|
`cron_daily` delle 00:30) → `scripts/live/revisione.py --quiet` → `src/live/revisione.scrivi`. Materiale
|
||||||
|
(~195k caratteri, sola lettura, ordine: regole e stato dichiarato, cio' che gira, cio' che e' successo):
|
||||||
|
CLAUDE.md intero, `config/live.json`, `crontab -l`, `git log` 14 g + `git status`, `monitor_health`,
|
||||||
|
stato dei sorveglianti (scale, vrp_f, usde, balance, venue_news, movimenti dichiarati, watermark — messi
|
||||||
|
PRIMA delle voci grandi, cosi' il tetto totale non li taglia mai: alla prima stesura `usde_convert` era
|
||||||
|
finito a 2 caratteri), 7 pagine di giornale, i diari della settimana. Prompt con dieci regole: sola
|
||||||
|
lettura, ogni segnalazione cita la fonte, solo numeri del materiale (guardia `numeri_non_supportati` di
|
||||||
|
`analista`, soglia 12), le decisioni vincolanti di §3 non si ripropongono, un'idea nuova si propone solo
|
||||||
|
citando la memoria che ha ucciso la simile, i gate si elencano con data e criterio senza anticipare il
|
||||||
|
verdetto, misurato ≠ dedotto, niente previsioni, URGENTE per cio' che serve all'operatore subito, cinque
|
||||||
|
titoli fissi. Rapporto in `docs/revisioni/<data>.md` firmato (modello, data, «lettore fallibile P13»,
|
||||||
|
tabella del materiale letto con caratteri e troncature); Telegram = la sola Sintesi, con taglio dichiarato.
|
||||||
|
Modello `claude-fable-5-1` via `analista.interroga` (CLI `claude -p`, `--allowed-tools ""`, timeout 900 s):
|
||||||
|
**non il modello che scrive il codice**, ed e' un test (`test_il_revisore_non_e_il_modello_che_scrive_il_codice`).
|
||||||
|
Un modello muto o una risposta senza i titoli produce un rapporto «NON eseguita» col motivo e un 🚨: il
|
||||||
|
silenzio non e' una revisione (P5). Guardie: `valida` di `cli` come prima istruzione (`test_cli_flag` lo
|
||||||
|
deriva dalla cartella), `--secco` non chiama e non scrive (test sull'albero dei file), un giro scrive **UN
|
||||||
|
solo file** (test), la cadenza e' dichiarata nel `.sh` e letta dalla crontab installata (`test_revisione`,
|
||||||
|
stesso schema di `test_book_cadenza`). Costo: una chiamata a settimana. Cio' che il sorvegliante non puo'
|
||||||
|
fare: eseguire. E' voluto.
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Revisione settimanale — 2026-09-09
|
||||||
|
|
||||||
|
*Scritta il 2026-09-09T21:59:07Z dal modello `claude-fable-5-1`, revisore diverso dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: **sospetta** — 11 numeri non presenti nel materiale: 1.011, 1.341, 1.38279, 1.40085, 2.034, 2.43408, 4.47099, 4.47746, 400, 40008, 6879.*
|
||||||
|
|
||||||
|
## Sintesi
|
||||||
|
|
||||||
|
Revisione del 2026-09-09 21:59Z (primo giro, manuale: il cron e' del lunedi'). **Nessun URGENTE.** Nella settimana 02-08/09 il libro ha fatto 24/24 giri ogni giorno con feed SKH a 0 min, leva lorda 0,26x, equity $4.477,46 e TWR +7,41% (giornale 08/09). Tre cose da verificare, in ordine:
|
||||||
|
|
||||||
|
1. **`usde_watch` del 07/09 ha stimato un reward di 400,08 USDE**, dove i giorni normali (08-09/09) danno 0,38. E' la differenza fra i 2.434,08 USDE entrati e i 2.034,0 contati come trade, con `n_trades` fermo a **50**. Il registro `usde_convert.jsonl` mostra 8 + 30 + 16 = 54 ordini: ne mancano 4 da 100 = 400. Un lettore che vede al massimo 50 trade attribuisce al reward cio' che era un acquisto (D2, P2). Se `rendimento()` usa quella finestra, il tasso che `quota_da_decidere()` stampa ogni giorno e' inquinato.
|
||||||
|
2. **Il cuscino USDC (debito §5.17) resta senza sorvegliante, e la sua misura non e' definita**: a `balance_watch` 21:42Z l'USDC ha balance 1.400,85 ma equity 1.382,79; con l'equity totale 4.470,99 (watermark) il cuscino al 30% vale ~$1.341, quindi lo slack e' **+$59 sul balance o +$41 sull'equity USDC**. Quale dei due conta va deciso prima di scrivere il sorvegliante.
|
||||||
|
3. **Operatore**: il versamento di $2.410,14 del 04/09 e' solo *rilevato* (salto 13:47→14:47); `movimenti_dichiarati.jsonl` contiene il solo €25 del 25/08 e la Nota di giornale e' vuota 7/7 giorni. CLAUDE.md §2 chiede la dichiarazione il giorno stesso, e §1 («soldi fermi») dice ancora «in attesa di importo e data» per il piano €5.000: non si capisce se il 04/09 ne e' il primo pezzo.
|
||||||
|
|
||||||
|
## Cosa non torna
|
||||||
|
|
||||||
|
- **«2.434 USDE in 39 ordini»** (diario `2026-09-06-usde-quota-topup` §3, CLAUDE.md §1) non si riproduce dal registro: 8 ordini nel primo record, 30 eventi nella prima sonda, 16 nella seconda. Il totale USDE torna, il conteggio no (N11).
|
||||||
|
- **Haircut, due affermazioni opposte in CLAUDE.md §1** (M28): una dice che il 5% e' confermato dal passaggio a X:SM (`available_funds` $2.011,09 contro $2.010,90 attesi *con* haircut 5%); poche righe sotto «oggi comunque non si applica», frase del mattino del 31/08 sotto S:SM. `config/live.json` `_nota_usde` dice ancora «il modello ATTIVO e' Segregated» e giustifica `depeg_crit 0.95` come «meta' del buffer di haircut (10%)»: col 5% il criterio dichiarato (P6) non descrive piu' la soglia, e la nota e' una diagnosi cablata (P4).
|
||||||
|
- **GATE SCALA-01: data e condizione A2 non concordano** (CLAUDE.md §4). «Non prima del 2026-10-01» sono 30 giorni dal 01/09; A2 chiede k0 «≥180 g col criterio passato ogni giorno», e il criterio lo misura solo `scale_watch`, attivo dal 01/09 (`scale_history.jsonl`). 180 giorni dal 01/09 cadono nel 2027; dal 20/06 a fine 2026. Da quando si conta va chiarito prima del 01/10, non quel giorno.
|
||||||
|
- **Sorveglianti di cui il materiale non prova il funzionamento** (P5): `venue_news.jsonl` ha 5 voci tutte `seed: true` viste il 2026-08-31T07:40Z e nulla dopo; `scale_watch_state.json` non ha timestamp (streak 0 puo' essere «non ho girato»); `edge_watch` e' citato a «stato 27/07» (CLAUDE.md §4); `fee_watch` non compare. `monitor_health` copre 8 monitor, nessuno di questi quattro.
|
||||||
|
- **Data di arming**: CLAUDE.md §0 «LIVE dal 2026-06-20», `config/live.json` «ARMATO 2026-06-23», primo segmento TWR dal 2026-06-23 (diario `2026-09-02-debito-14`). Una delle tre non e' la stessa data.
|
||||||
|
- **Richieste dell'analista senza risposta**: i 4 fill del 03/09 («dovrebbero essere ri-taglie»), la conferma del versamento 04/09, il SELL $−7 del 06/09, il round-trip a $+0,00 con fee 0,0055 del 08/09. Nessuna Nota. Non e' un guasto: e' un canale che non chiude (N9).
|
||||||
|
- **CLAUDE.md §13 dice 1.011 test**; il diario `2026-09-09c` ne aggiunge 16 (`test_revisione.py`). E `scripts/cron_review.sh`, `scripts/live/revisione.py`, `src/live/revisione.py` sono `??` in `git status` mentre la crontab li chiama lunedi' 06:15 (P16 li fa girare, P11 non li salva).
|
||||||
|
|
||||||
|
## Cosa e' maturo da decidere
|
||||||
|
|
||||||
|
| gate | data | criterio scritto | stato nel materiale |
|
||||||
|
|---|---|---|---|
|
||||||
|
| STATARB | 2026-09-27 (fra 18 g) | soglie 24/07 + diagnostica sempre-short | monitor OK; Sharpe −1,61 al 26/08, nessuna lettura piu' recente. Si legge il 27/09 |
|
||||||
|
| SCALA-01 | non prima del 2026-10-01 (22 g) | 9 condizioni tutte necessarie | A8/A9 fatti; A2 non maturo e ambiguo (sopra); A3, A6, A7 non risultano fatti |
|
||||||
|
| USDE-01 | chiuso 28/08 | — | quota 68,79% (09/09) sotto `quota_max_frac` 0,85: dentro la decisione dell'operatore del 30/08 e 06/09 |
|
||||||
|
| PREVDAY-01 | **senza data** | 10 condizioni, 8/10 | N10: un gate senza data non si chiude |
|
||||||
|
|
||||||
|
XSR01 (23/10) e DVOLSPREAD (24/10) cadono oltre 30 giorni. `edge_watch` e' continuo ma l'ultimo stato e' del 27/07: non determinabile se sia ancora 8/8.
|
||||||
|
|
||||||
|
Decisioni dell'operatore, oggi: (a) dichiarare il 04/09; (b) dire se il piano €5.000 e' iniziato; (c) definire il cuscino (balance o equity USDC). Nessuna decisione di §3 ha la colonna «cosa la riapre» soddisfatta: `weights_tilt_null` fallisce ancora (26/07, in-sample); il crollo catturato e' a vol bassa, sotto il gate e inverse (diario 09/09 §2).
|
||||||
|
|
||||||
|
## Proposte (nessuna azione eseguita)
|
||||||
|
|
||||||
|
1. **Agente**: verificare in `usde_watch` il limite di lettura dei trade e come `rendimento()` tratta la finestra del 07/09; rigenerare la stima con i 54 ordini (P2).
|
||||||
|
2. **Agente**: riallineare `_nota_usde` (modello attivo X:SM, criterio di `depeg_crit` al 5%, `venue_cap_frac` null) e sciogliere la contraddizione di §1 datando le due frasi (M28).
|
||||||
|
3. **Agente + operatore**: il sorvegliante del cuscino e' gia' descritto in §5.17 (`USDC − cuscino_richiesto` derivato da config, P1); manca solo la scelta balance/equity.
|
||||||
|
4. **Agente**: uno stato senza timestamp non distingue «zero» da «non ho girato» (P5): `scale_watch_state.json` e `venue_news` meritano l'ora dell'ultimo giro, e i quattro sorveglianti fuori da `monitor_health` una riga li'.
|
||||||
|
5. **Agente**: committare i file della revisione, aggiornare il conteggio test.
|
||||||
|
6. **Operatore**: dare una data a PREVDAY-01 o chiuderlo (N10).
|
||||||
|
7. Nessuna idea di strategia: la settimana ha gia' misurato crolli, opzioni e XRP contro `20-ondate` (diario 09/09 §0) e non ho nulla che batta i motivi elencati li'.
|
||||||
|
|
||||||
|
## Cosa ho letto e cosa mi mancava
|
||||||
|
|
||||||
|
Letto: CLAUDE.md intero; `config/live.json`; crontab; git log dal 26/08 e status; `monitor_health`; stati di scale_watch, scale_history, vrp_f_watch, usde_watch, usde_convert, balance_watch, venue_news, movimenti_dichiarati, watermark; giornali 02-08/09; diari 02/09 (tre), 06/09, 09/09 (tre).
|
||||||
|
|
||||||
|
Mancava: (1) stato con timestamp di `edge_watch`, `fee_watch`, `venue_watch`, `scale_watch`; (2) l'output di `usde_watch.rendimento()` e `quota_da_decidere()`; (3) la lettura corrente di `paper_statarb` (Sharpe a oggi, non al 26/08), dvolspread, xsr; (4) il contenuto di `cron_daily.sh`, cioe' quali sorveglianti chiama; (5) `venue_news` dopo il 31/08; (6) il diff di CLAUDE.md e memoria 40 non committati; (7) il registro fill della settimana (per i 4 fill del 03/09 e il +0,00 del 08/09); (8) un registro depositi del gateway, che «non espone get_deposits».
|
||||||
|
|
||||||
|
---
|
||||||
|
## Firma e materiale
|
||||||
|
|
||||||
|
- revisore: `claude-fable-5-1` · cadenza settimanale · giornale 7 g · diari 7 g · commit 14 g
|
||||||
|
- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file scritto e' questo
|
||||||
|
|
||||||
|
| voce | caratteri | troncata |
|
||||||
|
|---|---|---|
|
||||||
|
| CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole) | 80,310 | |
|
||||||
|
| config/live.json (unica autorita' dei guardrail live) | 7,184 | |
|
||||||
|
| crontab installata (crontab -l) | 1,312 | |
|
||||||
|
| git log dal 2026-08-26 + stato del working tree | 4,543 | |
|
||||||
|
| monitor_health (i forward-monitor stanno registrando?) | 522 | |
|
||||||
|
| stato scale_watch_state.json | 149 | |
|
||||||
|
| stato scale_history.jsonl (ultime 3) | 259 | |
|
||||||
|
| stato vrp_f_watch/state.json | 453 | |
|
||||||
|
| stato usde_watch.jsonl (ultime 3) | 1,339 | |
|
||||||
|
| stato usde_convert.jsonl (ultime 3) | 9,979 | |
|
||||||
|
| stato balance_watch.jsonl (ultime 2) | 578 | |
|
||||||
|
| stato venue_news.jsonl (ultime 5) | 1,564 | |
|
||||||
|
| stato movimenti_dichiarati.jsonl | 405 | |
|
||||||
|
| stato equity_seen.json (watermark) | 73 | |
|
||||||
|
| giornale 2026-09-02 | 3,824 | |
|
||||||
|
| giornale 2026-09-03 | 3,745 | |
|
||||||
|
| giornale 2026-09-04 | 3,999 | |
|
||||||
|
| giornale 2026-09-05 | 3,645 | |
|
||||||
|
| giornale 2026-09-06 | 3,630 | |
|
||||||
|
| giornale 2026-09-07 | 3,846 | |
|
||||||
|
| giornale 2026-09-08 | 3,765 | |
|
||||||
|
| diario 2026-09-02-debito-14-report-twr.md | 6,952 | |
|
||||||
|
| diario 2026-09-02b-debito-7-cadenza-docstring.md | 6,061 | |
|
||||||
|
| diario 2026-09-02c-flag-e-pulizia.md | 5,874 | |
|
||||||
|
| diario 2026-09-06-usde-quota-topup.md | 6,571 | |
|
||||||
|
| diario 2026-09-09-crolli-opzioni-monete.md | 17,894 | |
|
||||||
|
| diario 2026-09-09b-debito-18-cblib-causale.md | 14,150 | |
|
||||||
|
| diario 2026-09-09c-revisione-settimanale.md | 5,514 | |
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Revisione SETTIMANALE in sola lettura (dal 2026-09-09): un revisore diverso dall'autore
|
||||||
|
# (claude-fable-5-1) rilegge CLAUDE.md, config, crontab, commit, monitor_health, giornale e diari,
|
||||||
|
# e scrive docs/revisioni/<data>.md + una sintesi Telegram. Propone, non esegue: l'unico file
|
||||||
|
# scritto e' il rapporto. Cadenza: lunedi' 06:15 UTC — fuori dal minuto :00 (rate-limit) e dallo
|
||||||
|
# slot di release Deribit (martedi' 09:00), dopo il cron_daily delle 00:30 (giornale di domenica chiuso).
|
||||||
|
export PATH="/home/adriano/.local/bin:$PATH"
|
||||||
|
cd /opt/docker/PythagorasGoal || exit 1
|
||||||
|
mkdir -p logs
|
||||||
|
{
|
||||||
|
echo "===== $(date -u '+%Y-%m-%dT%H:%M:%SZ') cron_review ====="
|
||||||
|
uv run python scripts/live/revisione.py --quiet || true
|
||||||
|
echo "===== done $(date -u '+%H:%M:%SZ') ====="
|
||||||
|
} >> logs/cron_review.log 2>&1
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Revisione settimanale in SOLA LETTURA: un modello DIVERSO dall'autore rilegge il sistema.
|
||||||
|
|
||||||
|
uv run python scripts/live/revisione.py # chiama il revisore, scrive docs/revisioni/<data>.md, Telegram
|
||||||
|
uv run python scripts/live/revisione.py --secco # solo la taglia del materiale: niente modello, niente file
|
||||||
|
uv run python scripts/live/revisione.py --quiet # per il cron (scripts/cron_review.sh, lunedi' 06:15 UTC)
|
||||||
|
|
||||||
|
Cosa scrive: SOLO il rapporto. Cosa non tocca: config, libro, cron, codice, dati. Le proposte
|
||||||
|
restano proposte; decide l'operatore. Dettagli e ragioni in `src/live/revisione.py`.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
USO = """uso: revisione.py [--giorno AAAA-MM-GG] [--modello NOME] [--secco] [--no-telegram] [--quiet]
|
||||||
|
--giorno G data della revisione (default: adesso, UTC)
|
||||||
|
--modello M revisore (default: src.live.revisione.MODELLO = claude-fable-5-1 — NON il modello che scrive il codice)
|
||||||
|
--secco stampa la taglia del materiale e si ferma: niente modello, niente file, niente Telegram
|
||||||
|
--no-telegram scrive il rapporto ma non manda la sintesi
|
||||||
|
--quiet stampa solo l'esito
|
||||||
|
⚠️ SPENDE una chiamata al modello e MANDA un messaggio Telegram, salvo --secco/--no-telegram.
|
||||||
|
Scrive un solo file: docs/revisioni/<data>.md."""
|
||||||
|
|
||||||
|
|
||||||
|
def _arg(flag: str, default=None):
|
||||||
|
a = sys.argv[1:]
|
||||||
|
return a[a.index(flag) + 1] if flag in a and a.index(flag) + 1 < len(a) else default
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
from src.live.cli import valida
|
||||||
|
valida("revisione.py", USO, flag=("--secco", "--no-telegram", "--quiet"),
|
||||||
|
con_valore=("--giorno", "--modello"))
|
||||||
|
from src.live import revisione as R
|
||||||
|
g = _arg("--giorno")
|
||||||
|
adesso = (datetime.fromisoformat(g).replace(tzinfo=timezone.utc) if g
|
||||||
|
else datetime.now(timezone.utc))
|
||||||
|
esito = R.scrivi(ROOT, adesso, modello=_arg("--modello", R.MODELLO),
|
||||||
|
secco="--secco" in sys.argv, telegram="--no-telegram" not in sys.argv,
|
||||||
|
quiet="--quiet" in sys.argv)
|
||||||
|
if "--quiet" in sys.argv and not esito.get("secco"):
|
||||||
|
print(f"revisione {esito['data']}: {esito.get('stato')}"
|
||||||
|
+ (f" — {esito.get('errore')}" if esito.get("errore") else "") + f" {esito.get('file')}")
|
||||||
|
raise SystemExit(0 if esito.get("secco") or esito.get("stato") in ("ok", "sospetta") else 1)
|
||||||
@@ -0,0 +1,346 @@
|
|||||||
|
"""Revisione settimanale in SOLA LETTURA: un revisore diverso dall'autore rilegge il sistema.
|
||||||
|
|
||||||
|
PERCHE' ESISTE (2026-09-09). Nove sorveglianti in cron guardano ognuno la propria grandezza
|
||||||
|
(venue, fee, edge, scala, USDE, VRP-f, health, balance, paper-monitor); il giornale e l'analista
|
||||||
|
leggono UN giorno. Nessuno rilegge il sistema nel suo insieme: stato, gate con le date, decisioni
|
||||||
|
vincolanti, debiti aperti, cosa e' cambiato nei diari e nei commit. La revisione con un secondo
|
||||||
|
modello (regola del 2026-09-03: chi rivede non e' chi ha scritto) ha trovato in una sessione due
|
||||||
|
conclusioni false e un look-ahead che l'autore aveva riletto tre volte. Questo modulo la schedula.
|
||||||
|
|
||||||
|
COSA FA. Raccoglie il materiale (CLAUDE.md, config, crontab, log git, monitor_health, giornale
|
||||||
|
degli ultimi giorni, diari recenti, stato dei sorveglianti), lo da' a un modello DIVERSO da quello
|
||||||
|
che scrive il codice, e salva il rapporto FIRMATO in `docs/revisioni/<data>.md`, con una sintesi su
|
||||||
|
Telegram. Il rapporto e' l'opinione di un lettore fallibile (P13): dice cosa non torna, cosa e'
|
||||||
|
maturo da decidere, cosa propone. Non e' parte del registro delle misure.
|
||||||
|
|
||||||
|
COSA NON FA, PER COSTRUZIONE. Non tocca config, libro, cron, codice, dati: l'unico file che
|
||||||
|
scrive e' il rapporto. Non esegue proposte: le scrive, e l'operatore decide. Non riapre le
|
||||||
|
decisioni vincolanti (CLAUDE.md §3) se la colonna «cosa la riapre» non e' soddisfatta, e non
|
||||||
|
propone «idee nuove» senza citare il file di memoria che ha gia' ucciso l'idea simile. Un ciclo
|
||||||
|
che si autoregola e' esattamente cio' che `scale_watch` e `venue_watch` trattano come anomalia.
|
||||||
|
|
||||||
|
Il silenzio non e' una revisione: se il modello non risponde, il rapporto lo dice e Telegram lo
|
||||||
|
dice, invece di lasciare un lunedi' vuoto che si legge come «tutto bene».
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import html
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from datetime import date, datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.live import analista as A
|
||||||
|
from src.live import monitor_health as MH
|
||||||
|
|
||||||
|
MODELLO = "claude-fable-5-1" # il revisore: NON il modello che scrive il codice
|
||||||
|
CADENZA = "settimanale"
|
||||||
|
FINESTRA_GIORNALE_G = 7 # pagine del giornale lette
|
||||||
|
FINESTRA_DIARI_G = 7 # diari letti (la settimana: e' una revisione settimanale)
|
||||||
|
FINESTRA_COMMIT_G = 14 # commit letti
|
||||||
|
CARTELLA = Path("docs") / "revisioni"
|
||||||
|
MAX_CHARS_VOCE = 90_000 # CLAUDE.md sta sotto; il resto e' molto piu' piccolo
|
||||||
|
MAX_CHARS_TOTALE = 400_000
|
||||||
|
MAX_NUMERI_LIBERI = 12 # una revisione confronta e sottrae: qualche numero suo e' lecito
|
||||||
|
LIMITE_TELEGRAM = 3_500 # sotto i 4.096 di Telegram, con la testata
|
||||||
|
TIMEOUT_S = 900 # il materiale e' grande: 5 minuti non bastano sempre
|
||||||
|
|
||||||
|
TITOLI = ("## Sintesi", "## Cosa non torna", "## Cosa e' maturo da decidere",
|
||||||
|
"## Proposte (nessuna azione eseguita)", "## Cosa ho letto e cosa mi mancava")
|
||||||
|
|
||||||
|
ISTRUZIONI = """Sei il REVISORE settimanale di un progetto di trading quantitativo (BTC/ETH su Deribit, libro
|
||||||
|
live con soldi veri, ~$4.500). Sei un modello DIVERSO da quello che scrive codice e analisi del
|
||||||
|
progetto: il tuo valore e' che non condividi il suo ragionamento. Leggi il materiale qui sotto e
|
||||||
|
scrivi la revisione, in italiano, per l'operatore (che decide) e per l'agente (che esegue).
|
||||||
|
|
||||||
|
REGOLE VINCOLANTI:
|
||||||
|
1. SOLA LETTURA. Non proponi comandi da eseguire ora, non scrivi codice: descrivi cosa non torna
|
||||||
|
e cosa andrebbe verificato o deciso, e da chi.
|
||||||
|
2. OGNI SEGNALAZIONE CITA LA FONTE: file e sezione (es. «CLAUDE.md §4, gate STATARB»), data della
|
||||||
|
pagina di giornale, nome del diario, hash del commit. Una segnalazione senza fonte non vale.
|
||||||
|
3. USA SOLO I NUMERI DEL MATERIALE. Puoi sottrarre e confrontare, ma ogni cifra che scrivi deve
|
||||||
|
essere rintracciabile qui sotto; un controllo automatico elenchera' quelle che non lo sono.
|
||||||
|
4. LE DECISIONI VINCOLANTI (CLAUDE.md §3) NON SI RIPROPONGONO. Puoi dire che una condizione della
|
||||||
|
colonna «cosa la riapre» sembra soddisfatta, citando il dato; non puoi consigliare di cambiare
|
||||||
|
la decisione per altri motivi.
|
||||||
|
5. UN'IDEA «NUOVA» SI PROPONE SOLO citando il file di memoria (docs/memory/…) o il paragrafo che ha
|
||||||
|
gia' ucciso l'idea piu' simile, e dicendo in che cosa questa differisce. Se non lo sai, non la
|
||||||
|
proponi: scrivi «da verificare in memoria prima di proporla».
|
||||||
|
6. I GATE HANNO UNA DATA (CLAUDE.md §4). Elenca quelli la cui data cade entro 30 giorni o e' gia'
|
||||||
|
passata, con il criterio scritto e lo stato che il materiale mostra. Non anticipare un verdetto:
|
||||||
|
un gate si decide alla sua data, coi criteri scritti prima.
|
||||||
|
7. DISTINGUI misurato da dedotto. «Non determinabile da questo materiale» e' una risposta buona.
|
||||||
|
8. NIENTE PREVISIONI di mercato, niente consigli di direzione, niente target.
|
||||||
|
9. Se qualcosa richiede l'operatore SUBITO (fondi, credenziali, un sorvegliante fermo che copre un
|
||||||
|
gate, una modifica di config non registrata), mettilo nella Sintesi con la parola URGENTE.
|
||||||
|
10. Massimo ~1.000 parole. Usa ESATTAMENTE questi titoli, in quest'ordine, e nient'altro prima:
|
||||||
|
%s
|
||||||
|
Sotto «Cosa ho letto e cosa mi mancava» di' quali fonti hai usato e quale dato ti sarebbe
|
||||||
|
servito e non c'era (e' l'elenco da cui il materiale della prossima settimana migliora).
|
||||||
|
""" % "\n ".join(TITOLI)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- materiale
|
||||||
|
|
||||||
|
def _tronca(testo: str, n: int | None = None) -> tuple[str, bool]:
|
||||||
|
n = MAX_CHARS_VOCE if n is None else n # letto a runtime: un default e' catturato alla definizione (debito 12)
|
||||||
|
if len(testo) <= n:
|
||||||
|
return testo, False
|
||||||
|
return testo[:n] + f"\n[… troncato a {n:,} caratteri su {len(testo):,}]", True
|
||||||
|
|
||||||
|
|
||||||
|
def _cmd(args: list[str], cwd: Path) -> str:
|
||||||
|
"""Uscita di un comando di SOLA LETTURA, o il motivo per cui non si legge (P5: «non vedo»
|
||||||
|
non e' «va tutto bene»)."""
|
||||||
|
try:
|
||||||
|
p = subprocess.run(args, cwd=str(cwd), capture_output=True, text=True, timeout=60)
|
||||||
|
except Exception as exc: # noqa: BLE001 — un comando di lettura che esplode si dichiara
|
||||||
|
return f"non leggibile: {type(exc).__name__}: {str(exc)[:120]}"
|
||||||
|
if p.returncode != 0:
|
||||||
|
return f"non leggibile: uscita {p.returncode}: {(p.stderr or p.stdout).strip()[:200]}"
|
||||||
|
return p.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def _coda(path: Path, n: int) -> str:
|
||||||
|
if not path.exists():
|
||||||
|
return "(assente)"
|
||||||
|
righe = path.read_text().splitlines()
|
||||||
|
return "\n".join(righe[-n:]) if righe else "(vuoto)"
|
||||||
|
|
||||||
|
|
||||||
|
def _data_nel_nome(p: Path) -> date | None:
|
||||||
|
try:
|
||||||
|
return date.fromisoformat(p.name[:10])
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _monitor_health(root: Path, adesso: datetime) -> str:
|
||||||
|
try:
|
||||||
|
righe = MH.check_all(root, now=adesso)
|
||||||
|
except Exception as exc: # noqa: BLE001 — si dichiara, non si tace (P3)
|
||||||
|
return f"non leggibile: {type(exc).__name__}: {exc}"
|
||||||
|
out = ["monitor | stato | perche' | gate a rischio"]
|
||||||
|
for r in righe:
|
||||||
|
out.append(f"{r.get('name')} | {r.get('status')} | {r.get('why')} | {r.get('gate') or '-'}")
|
||||||
|
return "\n".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def materiale(root: Path, adesso: datetime) -> list[tuple[str, str, bool]]:
|
||||||
|
"""Le voci (nome, testo, troncato) date al revisore. Tutto in SOLA LETTURA.
|
||||||
|
|
||||||
|
L'ordine e' quello in cui un lettore umano le aprirebbe: prima le regole e lo stato dichiarato,
|
||||||
|
poi cio' che gira (sorveglianti: piccoli, e per questo PRIMA delle voci grandi, cosi' il tetto
|
||||||
|
totale non li taglia mai), poi cio' che e' successo (giornale, diari).
|
||||||
|
"""
|
||||||
|
oggi = adesso.date()
|
||||||
|
voci: list[tuple[str, str]] = []
|
||||||
|
|
||||||
|
voci.append(("CLAUDE.md (stato, book, numeri, decisioni vincolanti, gate, debiti, regole)",
|
||||||
|
(root / "CLAUDE.md").read_text() if (root / "CLAUDE.md").exists() else "(assente)"))
|
||||||
|
cfg = root / "config" / "live.json"
|
||||||
|
voci.append(("config/live.json (unica autorita' dei guardrail live)",
|
||||||
|
cfg.read_text() if cfg.exists() else "(assente)"))
|
||||||
|
voci.append(("crontab installata (crontab -l)", _cmd(["crontab", "-l"], root)))
|
||||||
|
since = (oggi - timedelta(days=FINESTRA_COMMIT_G)).isoformat()
|
||||||
|
voci.append((f"git log dal {since} + stato del working tree",
|
||||||
|
_cmd(["git", "log", f"--since={since}", "--format=%h %ad %s", "--date=short"], root)
|
||||||
|
+ "\n--- git status --short ---\n" + _cmd(["git", "status", "--short"], root)))
|
||||||
|
voci.append(("monitor_health (i forward-monitor stanno registrando?)", _monitor_health(root, adesso)))
|
||||||
|
|
||||||
|
live = root / "data" / "live"
|
||||||
|
stati = {
|
||||||
|
"scale_watch_state.json": (live / "scale_watch_state.json", 999),
|
||||||
|
"scale_history.jsonl (ultime 3)": (live / "scale_history.jsonl", 3),
|
||||||
|
"vrp_f_watch/state.json": (root / "data" / "vrp_f_watch" / "state.json", 999),
|
||||||
|
"usde_watch.jsonl (ultime 3)": (live / "usde_watch.jsonl", 3),
|
||||||
|
"usde_convert.jsonl (ultime 3)": (live / "usde_convert.jsonl", 3),
|
||||||
|
"balance_watch.jsonl (ultime 2)": (live / "balance_watch.jsonl", 2),
|
||||||
|
"venue_news.jsonl (ultime 5)": (live / "venue_news.jsonl", 5),
|
||||||
|
"movimenti_dichiarati.jsonl": (live / "movimenti_dichiarati.jsonl", 50),
|
||||||
|
"equity_seen.json (watermark)": (live / "equity_seen.json", 999),
|
||||||
|
}
|
||||||
|
for nome, (p, n) in stati.items():
|
||||||
|
voci.append((f"stato {nome}", _coda(p, n)))
|
||||||
|
|
||||||
|
for i in range(FINESTRA_GIORNALE_G, 0, -1):
|
||||||
|
g = oggi - timedelta(days=i)
|
||||||
|
p = root / "docs" / "journal" / f"{g.isoformat()}.md"
|
||||||
|
if p.exists():
|
||||||
|
voci.append((f"giornale {g.isoformat()}", p.read_text()))
|
||||||
|
diari = sorted(p for p in (root / "docs" / "diary").glob("*.md")
|
||||||
|
if (_data_nel_nome(p) or date.min) >= oggi - timedelta(days=FINESTRA_DIARI_G))
|
||||||
|
for p in diari:
|
||||||
|
voci.append((f"diario {p.name}", p.read_text()))
|
||||||
|
|
||||||
|
out, tot = [], 0
|
||||||
|
for nome, testo in voci:
|
||||||
|
t, tr = _tronca(testo)
|
||||||
|
if tot + len(t) > MAX_CHARS_TOTALE:
|
||||||
|
t, tr = _tronca(t, max(0, MAX_CHARS_TOTALE - tot)), True
|
||||||
|
tot += len(t)
|
||||||
|
out.append((nome, t, tr))
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def costruisci_prompt(voci: list[tuple[str, str, bool]], adesso: datetime) -> str:
|
||||||
|
parti = [ISTRUZIONI, f"\nDATA DELLA REVISIONE: {adesso:%Y-%m-%d %H:%M}Z (usa questa per «oggi» e per le date dei gate).\n"]
|
||||||
|
for nome, testo, tr in voci:
|
||||||
|
parti.append(f"\n=== {nome}{' [TRONCATO]' if tr else ''} ===\n{testo}\n")
|
||||||
|
parti.append("\nScrivi ora la revisione, coi cinque titoli, e nient'altro.")
|
||||||
|
return "\n".join(parti)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- guardia
|
||||||
|
|
||||||
|
def valida(testo: str, fonte: str) -> tuple[str, list[str]]:
|
||||||
|
"""(stato, motivi) — stato in {ok, sospetta, rifiutata}. Rifiutata = non e' una revisione
|
||||||
|
(vuota o senza i titoli); sospetta = numeri non rintracciabili nel materiale, elencati."""
|
||||||
|
motivi: list[str] = []
|
||||||
|
t = testo.strip()
|
||||||
|
if not t:
|
||||||
|
return "rifiutata", ["risposta vuota"]
|
||||||
|
mancanti = [x for x in TITOLI if x not in t]
|
||||||
|
if mancanti:
|
||||||
|
return "rifiutata", [f"titoli mancanti: {', '.join(mancanti)}"]
|
||||||
|
fuori = A.numeri_non_supportati(t, fonte)
|
||||||
|
if fuori:
|
||||||
|
motivi.append(f"{len(fuori)} numeri non presenti nel materiale: " + ", ".join(fuori[:30]))
|
||||||
|
if len(fuori) > MAX_NUMERI_LIBERI:
|
||||||
|
return "sospetta", motivi
|
||||||
|
return ("sospetta" if motivi else "ok"), motivi
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- rapporto
|
||||||
|
|
||||||
|
def rapporto(testo: str | None, errore: str | None, stato: str, motivi: list[str], modello: str,
|
||||||
|
adesso: datetime, voci: list[tuple[str, str, bool]]) -> str:
|
||||||
|
testa = [f"# Revisione {CADENZA} — {adesso:%Y-%m-%d}", ""]
|
||||||
|
if errore:
|
||||||
|
testa += [f"## Revisione NON eseguita",
|
||||||
|
"",
|
||||||
|
f"Il modello `{modello}` non ha risposto: **{errore}**. Non c'e' una revisione questa "
|
||||||
|
"settimana, e questo file esiste perche' un lunedi' senza rapporto si leggerebbe "
|
||||||
|
"come «tutto bene» (P5).", ""]
|
||||||
|
corpo = []
|
||||||
|
else:
|
||||||
|
testa += [f"*Scritta il {adesso:%Y-%m-%dT%H:%M:%S}Z dal modello `{modello}`, revisore diverso "
|
||||||
|
"dall'autore del codice, in SOLA LETTURA: nessun file del repo, nessuna config, nessun "
|
||||||
|
"ordine e' stato toccato. E' l'opinione di un lettore fallibile (P13): ogni numero e "
|
||||||
|
"ogni proposta vanno verificati alla fonte prima di agire. Guardia sui numeri: "
|
||||||
|
f"**{stato}**" + (f" — {'; '.join(motivi)}" if motivi else "") + ".*", ""]
|
||||||
|
corpo = [testo.strip(), ""]
|
||||||
|
coda = ["---", "## Firma e materiale", "",
|
||||||
|
f"- revisore: `{modello}` · cadenza {CADENZA} · giornale {FINESTRA_GIORNALE_G} g · "
|
||||||
|
f"diari {FINESTRA_DIARI_G} g · commit {FINESTRA_COMMIT_G} g",
|
||||||
|
"- scritto da `scripts/live/revisione.py` (cron `scripts/cron_review.sh`); l'unico file "
|
||||||
|
"scritto e' questo", "",
|
||||||
|
"| voce | caratteri | troncata |", "|---|---|---|"]
|
||||||
|
coda += [f"| {n} | {len(t):,} | {'si' if tr else ''} |" for n, t, tr in voci]
|
||||||
|
return "\n".join(testa + corpo + coda) + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def _sezione(testo: str, titolo: str) -> str:
|
||||||
|
if titolo not in testo:
|
||||||
|
return ""
|
||||||
|
dopo = testo.split(titolo, 1)[1]
|
||||||
|
for t in TITOLI:
|
||||||
|
if t != titolo and t in dopo:
|
||||||
|
dopo = dopo.split(t, 1)[0]
|
||||||
|
return dopo.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def per_telegram(testo: str | None, errore: str | None, stato: str, adesso: datetime,
|
||||||
|
path: Path) -> str:
|
||||||
|
"""La Sintesi, con la testata e il taglio DICHIARATO (un taglio silenzioso e' peggio di
|
||||||
|
un'assenza: si legge come un pensiero finito)."""
|
||||||
|
if errore:
|
||||||
|
return (f"<b>🚨 Revisione {CADENZA} NON eseguita</b> {adesso:%Y-%m-%d}\n"
|
||||||
|
f"{html.escape(errore)}\n<code>{html.escape(str(path))}</code>")
|
||||||
|
sintesi = _sezione(testo or "", TITOLI[0]) or "(nessuna Sintesi nel rapporto)"
|
||||||
|
corpo = html.escape(sintesi, quote=False)
|
||||||
|
tagliato = ""
|
||||||
|
if len(corpo) > LIMITE_TELEGRAM:
|
||||||
|
corpo, tagliato = corpo[:LIMITE_TELEGRAM], "\n[… sintesi tagliata: il resto e' nel file]"
|
||||||
|
return (f"<b>🔍 Revisione {CADENZA}</b> {adesso:%Y-%m-%d} · guardia numeri: {stato}\n\n"
|
||||||
|
f"{corpo}{tagliato}\n\n<code>{html.escape(str(path))}</code>")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- modello
|
||||||
|
|
||||||
|
def interroga(prompt: str, modello: str = MODELLO, timeout: int = TIMEOUT_S) -> tuple[str, str | None]:
|
||||||
|
"""(testo, errore) dalla CLI `claude -p`, col prompt su STDIN.
|
||||||
|
|
||||||
|
Non su argv: al primo giro reale (09/09, 195k caratteri) `analista.interroga` e' morto con
|
||||||
|
`OSError: Argument list too long` PRIMA di chiamare il modello — e l'eccezione, non catturata,
|
||||||
|
ha lasciato zero rapporto e zero Telegram: il silenzio che questo modulo esiste per vietare.
|
||||||
|
Qui ogni eccezione della chiamata e' un errore RESTITUITO (P3), mai propagato.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
p = subprocess.run(
|
||||||
|
["claude", "-p", "--model", modello, "--allowed-tools", "", "--max-turns", "1"],
|
||||||
|
input=prompt, capture_output=True, text=True, timeout=timeout)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return "", "CLI `claude` non trovata"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "", f"timeout dopo {timeout}s"
|
||||||
|
except Exception as exc: # noqa: BLE001 — si restituisce, non si tace
|
||||||
|
return "", f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
if p.returncode != 0:
|
||||||
|
return "", A.motivo_uscita(p.returncode, p.stdout, p.stderr)
|
||||||
|
return p.stdout.strip(), None # NON `analista.pulisci`: toglierebbe i titoli «## …» che la guardia richiede
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- orchestrazione
|
||||||
|
|
||||||
|
def scrivi(root: Path, adesso: datetime | None = None, modello: str = MODELLO, *,
|
||||||
|
secco: bool = False, telegram: bool = True, quiet: bool = False,
|
||||||
|
interroga=None, invia=None) -> dict:
|
||||||
|
"""Un giro: materiale → modello → rapporto → Telegram. Ritorna cosa e' successo.
|
||||||
|
|
||||||
|
`interroga(prompt, modello, timeout) -> (testo, errore)` e `invia(html) -> bool` sono
|
||||||
|
INIETTATI (default: `interroga` di questo modulo — prompt su stdin — e `notifier.send`) cosi'
|
||||||
|
i test non spendono e non mandano. Se uno dei due SOLLEVA, l'eccezione diventa un errore nel
|
||||||
|
rapporto: un giro che muore a meta' non deve lasciare un lunedi' vuoto. Con `secco` non si chiama il modello e non si scrive niente: si stampa la taglia.
|
||||||
|
"""
|
||||||
|
adesso = adesso or datetime.now(timezone.utc)
|
||||||
|
voci = materiale(root, adesso)
|
||||||
|
prompt = costruisci_prompt(voci, adesso)
|
||||||
|
esito = {"data": adesso.date().isoformat(), "voci": len(voci), "caratteri": len(prompt),
|
||||||
|
"modello": modello, "secco": secco}
|
||||||
|
if not quiet:
|
||||||
|
print(f" revisione {esito['data']} modello {modello} materiale {len(voci)} voci, "
|
||||||
|
f"{len(prompt):,} caratteri" + (" [SECCO: nessuna chiamata, nessuna scrittura]" if secco else ""))
|
||||||
|
for n, t, tr in voci:
|
||||||
|
print(f" {len(t):>8,} {n}{' [troncata]' if tr else ''}")
|
||||||
|
if secco:
|
||||||
|
return esito
|
||||||
|
|
||||||
|
interroga = interroga or globals()["interroga"]
|
||||||
|
try:
|
||||||
|
testo, errore = interroga(prompt, modello, TIMEOUT_S)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
testo, errore = "", f"eccezione nella chiamata al modello: {type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
stato, motivi = ("rifiutata", [errore]) if errore else valida(testo, prompt)
|
||||||
|
if not errore and stato == "rifiutata":
|
||||||
|
errore, testo = "risposta non e' una revisione: " + "; ".join(motivi), None
|
||||||
|
|
||||||
|
cartella = root / CARTELLA
|
||||||
|
cartella.mkdir(parents=True, exist_ok=True)
|
||||||
|
path = cartella / f"{adesso:%Y-%m-%d}.md"
|
||||||
|
path.write_text(rapporto(testo, errore, stato, motivi, modello, adesso, voci))
|
||||||
|
esito.update(stato=stato, errore=errore, motivi=motivi, file=str(path))
|
||||||
|
|
||||||
|
if telegram:
|
||||||
|
from src.live import notifier as N
|
||||||
|
invia = invia or (lambda h: N.send(h, tentativi=2))
|
||||||
|
try:
|
||||||
|
esito["telegram"] = bool(invia(per_telegram(testo, errore, stato, adesso, path)))
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
esito["telegram"] = False
|
||||||
|
esito["telegram_errore"] = f"{type(exc).__name__}: {str(exc)[:200]}"
|
||||||
|
if not quiet:
|
||||||
|
tg = "" if "telegram" not in esito else (" telegram: inviato" if esito["telegram"] else
|
||||||
|
f" telegram: NON inviato ({esito.get('telegram_errore', 'send False')})")
|
||||||
|
print(f" -> {stato}" + (f" ({errore})" if errore else "") + f" {path}{tg}")
|
||||||
|
return esito
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
"""Revisione settimanale (src/live/revisione.py): sola lettura, firmata, e il silenzio non e' un esito.
|
||||||
|
|
||||||
|
Nessun test chiama il modello o Telegram: `interroga` e `invia` sono iniettati. Nessun test scrive
|
||||||
|
nel repo: tutto in una radice temporanea con file sintetici.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
from src.live import revisione as R # noqa: E402
|
||||||
|
from src.live.venue_probe import in_release_window # noqa: E402
|
||||||
|
|
||||||
|
ADESSO = datetime(2026, 9, 14, 6, 15, tzinfo=timezone.utc) # un lunedi'
|
||||||
|
|
||||||
|
|
||||||
|
def _radice(tmp_path: Path) -> Path:
|
||||||
|
(tmp_path / "CLAUDE.md").write_text("# CLAUDE\n\n## 4. Gate\n| STATARB | 2026-09-27 | soglie | ritiro |\n")
|
||||||
|
(tmp_path / "config").mkdir(); (tmp_path / "config" / "live.json").write_text('{"disaster_sl_pct": 0.30}')
|
||||||
|
j = tmp_path / "docs" / "journal"; j.mkdir(parents=True)
|
||||||
|
for i in range(1, 10):
|
||||||
|
g = (ADESSO - timedelta(days=i)).date().isoformat()
|
||||||
|
(j / f"{g}.md").write_text(f"# Giornale {g}\nEquity $4,477.46 · leva 0.26x\n")
|
||||||
|
d = tmp_path / "docs" / "diary"; d.mkdir()
|
||||||
|
(d / "2026-09-09b-recente.md").write_text("recente: f 0,712\n")
|
||||||
|
(d / "2026-08-20-vecchio.md").write_text("vecchio\n")
|
||||||
|
(d / "senza-data.md").write_text("senza data\n")
|
||||||
|
live = tmp_path / "data" / "live"; live.mkdir(parents=True)
|
||||||
|
(live / "scale_watch_state.json").write_text('{"scala": 1.0}')
|
||||||
|
(live / "usde_watch.jsonl").write_text("\n".join(f'{{"i": {i}}}' for i in range(10)) + "\n")
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- materiale
|
||||||
|
|
||||||
|
def test_il_materiale_prende_la_finestra_dichiarata_e_dice_cosa_non_legge(tmp_path):
|
||||||
|
voci = R.materiale(_radice(tmp_path), ADESSO)
|
||||||
|
nomi = [n for n, _, _ in voci]
|
||||||
|
giornali = [n for n in nomi if n.startswith("giornale ")]
|
||||||
|
assert len(giornali) == R.FINESTRA_GIORNALE_G, "7 pagine, non 9: la finestra e' dichiarata"
|
||||||
|
assert "giornale 2026-09-13" in nomi and "giornale 2026-09-07" in nomi and "giornale 2026-09-06" not in nomi
|
||||||
|
assert "diario 2026-09-09b-recente.md" in nomi
|
||||||
|
assert "diario 2026-08-20-vecchio.md" not in nomi and "diario senza-data.md" not in nomi
|
||||||
|
testo = dict((n, t) for n, t, _ in voci)
|
||||||
|
assert "0,712" in testo["diario 2026-09-09b-recente.md"]
|
||||||
|
assert testo["stato usde_watch.jsonl (ultime 3)"].splitlines() == ['{"i": 7}', '{"i": 8}', '{"i": 9}']
|
||||||
|
assert testo["stato scale_history.jsonl (ultime 3)"] == "(assente)", "un file assente si DICE (P5)"
|
||||||
|
assert testo["git log dal 2026-08-31 + stato del working tree"].startswith("non leggibile"), \
|
||||||
|
"in una cartella che non e' un repo il log non si inventa"
|
||||||
|
assert any(n.startswith("monitor_health") for n in nomi)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_sorveglianti_vengono_prima_delle_voci_grandi(tmp_path):
|
||||||
|
"""Il tetto totale taglia in coda: gli stati (piccoli) devono stare PRIMA di giornale e diari,
|
||||||
|
altrimenti sono i primi a sparire — e' successo alla prima stesura (usde_convert a 2 caratteri)."""
|
||||||
|
nomi = [n for n, _, _ in R.materiale(_radice(tmp_path), ADESSO)]
|
||||||
|
i_stato = max(i for i, n in enumerate(nomi) if n.startswith("stato "))
|
||||||
|
i_giornale = min(i for i, n in enumerate(nomi) if n.startswith("giornale "))
|
||||||
|
assert i_stato < i_giornale
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_troncamento_e_dichiarato(tmp_path, monkeypatch):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
(r / "CLAUDE.md").write_text("x" * 1000)
|
||||||
|
monkeypatch.setattr(R, "MAX_CHARS_VOCE", 100)
|
||||||
|
n, t, tr = R.materiale(r, ADESSO)[0]
|
||||||
|
assert tr and "[… troncato a 100 caratteri su 1,000]" in t
|
||||||
|
assert "[TRONCATO]" in R.costruisci_prompt([(n, t, tr)], ADESSO)
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_prompt_porta_le_regole_e_la_data(tmp_path):
|
||||||
|
p = R.costruisci_prompt(R.materiale(_radice(tmp_path), ADESSO), ADESSO)
|
||||||
|
assert "DATA DELLA REVISIONE: 2026-09-14 06:15Z" in p
|
||||||
|
for t in R.TITOLI:
|
||||||
|
assert t in p
|
||||||
|
for parola in ("SOLA LETTURA", "CITA LA FONTE", "DECISIONI VINCOLANTI", "docs/memory", "I GATE HANNO UNA DATA",
|
||||||
|
"NIENTE PREVISIONI"):
|
||||||
|
assert parola in p, parola
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- guardia
|
||||||
|
|
||||||
|
def _revisione(sintesi="Tutto letto. Equity $4,477.46.", extra=""):
|
||||||
|
return ("## Sintesi\n" + sintesi + "\n## Cosa non torna\nniente\n## Cosa e' maturo da decidere\n"
|
||||||
|
"STATARB il 2026-09-27\n## Proposte (nessuna azione eseguita)\nnessuna\n"
|
||||||
|
"## Cosa ho letto e cosa mi mancava\nCLAUDE.md" + extra)
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_guardia_rifiuta_il_vuoto_e_i_titoli_mancanti():
|
||||||
|
assert R.valida("", "fonte")[0] == "rifiutata"
|
||||||
|
stato, motivi = R.valida("## Sintesi\nsolo questa", "fonte")
|
||||||
|
assert stato == "rifiutata" and "titoli mancanti" in motivi[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_guardia_sui_numeri_ha_il_controllo_positivo():
|
||||||
|
fonte = "Equity $4,477.46 · leva 0.26x · 2026-09-27"
|
||||||
|
assert R.valida(_revisione(), fonte) == ("ok", [])
|
||||||
|
stato, motivi = R.valida(_revisione(sintesi="Equity 9.999,99 e leva 3,14x"), fonte)
|
||||||
|
assert stato == "sospetta" and "non presenti nel materiale" in motivi[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- rapporto e Telegram
|
||||||
|
|
||||||
|
def test_il_rapporto_e_firmato_e_dichiara_la_sola_lettura():
|
||||||
|
voci = [("CLAUDE.md", "x" * 10, False), ("giornale", "y", True)]
|
||||||
|
r = R.rapporto(_revisione(), None, "ok", [], "claude-fable-5-1", ADESSO, voci)
|
||||||
|
assert r.startswith("# Revisione settimanale — 2026-09-14")
|
||||||
|
for frase in ("`claude-fable-5-1`", "SOLA LETTURA", "lettore fallibile (P13)", "## Firma e materiale",
|
||||||
|
"| CLAUDE.md | 10 | |", "| giornale | 1 | si |", "l'unico file"):
|
||||||
|
assert frase in r, frase
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_modello_muto_produce_un_rapporto_che_lo_dice():
|
||||||
|
r = R.rapporto(None, "timeout dopo 900s", "rifiutata", ["timeout dopo 900s"], "m", ADESSO, [])
|
||||||
|
assert "## Revisione NON eseguita" in r and "timeout dopo 900s" in r and "P5" in r
|
||||||
|
tg = R.per_telegram(None, "timeout dopo 900s", "rifiutata", ADESSO, Path("docs/revisioni/x.md"))
|
||||||
|
assert tg.startswith("<b>🚨") and "timeout" in tg
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_porta_la_sintesi_e_dichiara_il_taglio():
|
||||||
|
tg = R.per_telegram(_revisione(sintesi="A <b>corta</b>."), None, "ok", ADESSO, Path("p.md"))
|
||||||
|
assert "A <b>corta</b>." in tg and "niente" not in tg, "solo la Sintesi, con l'HTML neutralizzato"
|
||||||
|
lunga = R.per_telegram(_revisione(sintesi="z" * 5000), None, "ok", ADESSO, Path("p.md"))
|
||||||
|
assert "[… sintesi tagliata" in lunga and len(lunga) < 4096
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- orchestrazione: cosa scrive, cosa no
|
||||||
|
|
||||||
|
def _albero(root: Path) -> set[str]:
|
||||||
|
return {str(p.relative_to(root)) for p in root.rglob("*") if p.is_file()}
|
||||||
|
|
||||||
|
|
||||||
|
def test_secco_non_chiama_e_non_scrive(tmp_path, capsys):
|
||||||
|
r = _radice(tmp_path); prima = _albero(r)
|
||||||
|
|
||||||
|
def mai(*a, **k):
|
||||||
|
raise AssertionError("il modello NON va chiamato in --secco")
|
||||||
|
esito = R.scrivi(r, ADESSO, secco=True, interroga=mai, invia=mai)
|
||||||
|
assert esito["secco"] and _albero(r) == prima
|
||||||
|
assert "SECCO" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_giro_riuscito_scrive_UN_solo_file_e_manda_la_sintesi(tmp_path):
|
||||||
|
r = _radice(tmp_path); prima = _albero(r); mandati = []
|
||||||
|
esito = R.scrivi(r, ADESSO, modello="claude-fable-5-1", quiet=True,
|
||||||
|
interroga=lambda p, m, t: (_revisione(), None), invia=lambda h: mandati.append(h) or True)
|
||||||
|
nuovi = _albero(r) - prima
|
||||||
|
assert nuovi == {"docs/revisioni/2026-09-14.md"}, "l'unico file scritto e' il rapporto"
|
||||||
|
assert esito["stato"] == "ok" and esito["telegram"] is True
|
||||||
|
assert "## Sintesi" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert len(mandati) == 1 and "Revisione settimanale" in mandati[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_un_errore_del_modello_scrive_il_rapporto_e_allerta(tmp_path):
|
||||||
|
r = _radice(tmp_path); mandati = []
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, interroga=lambda p, m, t: ("", "CLI `claude` non trovata"),
|
||||||
|
invia=lambda h: mandati.append(h) or True)
|
||||||
|
assert esito["stato"] == "rifiutata" and "non trovata" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert mandati and mandati[0].startswith("<b>🚨")
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_risposta_che_non_e_una_revisione_e_un_errore_dichiarato(tmp_path):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: ("ciao", None))
|
||||||
|
assert esito["stato"] == "rifiutata" and "titoli mancanti" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_telegram_non_manda(tmp_path):
|
||||||
|
r = _radice(tmp_path)
|
||||||
|
|
||||||
|
def mai(h):
|
||||||
|
raise AssertionError("Telegram NON va chiamato con telegram=False")
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, telegram=False, interroga=lambda p, m, t: (_revisione(), None), invia=mai)
|
||||||
|
assert "telegram" not in esito
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_prompt_va_su_stdin_non_su_argv(monkeypatch):
|
||||||
|
"""Al primo giro reale (09/09) 195k caratteri su argv hanno dato `Argument list too long` PRIMA
|
||||||
|
di chiamare il modello. DEVE FALLIRE SE: qualcuno rimette il prompt fra gli argomenti."""
|
||||||
|
visto = {}
|
||||||
|
|
||||||
|
class _P:
|
||||||
|
returncode, stdout, stderr = 0, _revisione(), ""
|
||||||
|
|
||||||
|
def finto_run(args, **kw):
|
||||||
|
visto["args"], visto["input"] = args, kw.get("input")
|
||||||
|
return _P()
|
||||||
|
monkeypatch.setattr(R.subprocess, "run", finto_run)
|
||||||
|
prompt = "PROMPT " * 40_000
|
||||||
|
testo, errore = R.interroga(prompt, "claude-fable-5-1", 5)
|
||||||
|
assert errore is None and "## Sintesi" in testo
|
||||||
|
assert visto["input"] == prompt and all(len(a) < 200 for a in visto["args"])
|
||||||
|
assert "--model" in visto["args"] and "claude-fable-5-1" in visto["args"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_una_eccezione_nella_chiamata_diventa_un_errore_non_un_crash(tmp_path, monkeypatch):
|
||||||
|
"""Controllo positivo sul difetto del primo giro: `OSError` dalla CLI → rapporto NON eseguita + 🚨."""
|
||||||
|
def esplode(args, **kw):
|
||||||
|
raise OSError(7, "Argument list too long")
|
||||||
|
monkeypatch.setattr(R.subprocess, "run", esplode)
|
||||||
|
assert R.interroga("x", "m", 5) == ("", "OSError: [Errno 7] Argument list too long")
|
||||||
|
r = _radice(tmp_path); mandati = []
|
||||||
|
|
||||||
|
def esplode_in_scrivi(p, m, t):
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
esito = R.scrivi(r, ADESSO, quiet=True, interroga=esplode_in_scrivi, invia=lambda h: mandati.append(h) or True)
|
||||||
|
assert esito["stato"] == "rifiutata" and "RuntimeError: boom" in esito["errore"]
|
||||||
|
assert "NON eseguita" in (r / "docs" / "revisioni" / "2026-09-14.md").read_text()
|
||||||
|
assert mandati and mandati[0].startswith("<b>🚨")
|
||||||
|
|
||||||
|
|
||||||
|
def test_il_revisore_non_e_il_modello_che_scrive_il_codice():
|
||||||
|
from src.live import analista as A
|
||||||
|
assert R.MODELLO == "claude-fable-5-1" and R.MODELLO != A.MODELLO_DEFAULT
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- cadenza: tre dichiarazioni d'accordo
|
||||||
|
|
||||||
|
def _riga_crontab() -> str | None:
|
||||||
|
try:
|
||||||
|
out = subprocess.run(["crontab", "-l"], capture_output=True, text=True, timeout=10)
|
||||||
|
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||||
|
return None
|
||||||
|
if out.returncode != 0:
|
||||||
|
return None
|
||||||
|
righe = [r for r in out.stdout.splitlines() if "cron_review.sh" in r and not r.lstrip().startswith("#")]
|
||||||
|
return righe[0] if righe else ""
|
||||||
|
|
||||||
|
|
||||||
|
def test_la_cadenza_dichiarata_nel_cron_sh_e_quella_installata_e_fuori_dagli_slot():
|
||||||
|
sh = (ROOT / "scripts" / "cron_review.sh").read_text()
|
||||||
|
m = re.search(r"lunedi' (\d\d):(\d\d) UTC", sh)
|
||||||
|
assert m, "cron_review.sh deve dichiarare «lunedi' HH:MM UTC»"
|
||||||
|
ora, minuto = int(m.group(1)), int(m.group(2))
|
||||||
|
assert minuto != 0, "mai al minuto tondo (rate-limit per-IP)"
|
||||||
|
prossimo = ADESSO.replace(hour=ora, minute=minuto) # ADESSO e' un lunedi'
|
||||||
|
assert prossimo.weekday() == 0 and not in_release_window(prossimo)
|
||||||
|
assert "revisione.py --quiet" in sh
|
||||||
|
riga = _riga_crontab()
|
||||||
|
if riga is None:
|
||||||
|
pytest.skip("crontab non leggibile: la dichiarazione installata non e' verificabile qui")
|
||||||
|
assert riga, "crontab leggibile ma cron_review.sh NON installato"
|
||||||
|
campi = riga.split()
|
||||||
|
assert campi[:5] == [str(minuto), str(ora), "*", "*", "1"], f"installata {campi[:5]}, dichiarata {minuto} {ora} * * 1"
|
||||||
Reference in New Issue
Block a user