ed2532b991
scripts/web/ — motore in JavaScript (engine.js) sui ritorni VERI del book esportati da export_series.py, pagine assemblate da build.py. Da "sistema dinamico dove imposti valore iniziale, mensile e durata, e calcola la best curva fissando il tempo": la simulazione gira nel browser, quindi il motore va riscritto e VA PROVATO che dia la stessa risposta. test_engine.js confronta mediane e probabilita' con r0807_growth_yearly.py su due configurazioni, esige che il versato (deterministico) coincida al centesimo, e include un controllo positivo — un motore col fisco spento DEVE risultare fuori tolleranza, perche' un test che non sa fallire non e' un test. Il risolutore e' validato contro dep_necessario di r0727_tasse.py: -0.4 / -0.6 / -1.1%. smoke.js ESEGUE le pagine con un DOM finto. Serve perche' node --check valida solo la sintassi: ho pubblicato una pagina che lo passava e moriva alla prima riga utile (chiavi Python "0.0" ricostruite in JS come String(0) = "0"; i pesi intermedi funzionavano per caso). Altri due errori che questi strumenti hanno intercettato: - due anni di dati di un grafico scritti A MEMORIA perche' tail aveva troncato l'output -> ora i dati si INIETTANO da JSON (build.py), il passaggio manuale non esiste piu'; - una "distorsione sistematica" del motore JS (+0.75%, 8 semi tutti positivi) che erano 8 estrazioni contro UN punto Python rumoroso. Misurato bene, 8 semi per parte: -0.01%, t = -0.04; ed entrambi i campionatori cadono entro 1.7 SE dall'atteso ANALITICO della media di blocco. Scelta guidata da una misura: la banda del versamento suggerito resta +-1% da 1.200 a 3.000 percorsi -> non domina il Monte Carlo ma la granularita' della bisezione (~5 EUR). Percorsi tenuti bassi e incertezza dichiarata, invece di pagare tempo per una precisione che non arriva. Nessun impatto sulla produzione: non tocca book, pesi, cron o config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
90 lines
3.3 KiB
Python
90 lines
3.3 KiB
Python
#!/usr/bin/env python
|
|
"""build.py — assembla le pagine pubblicate a partire da shell + dati prodotti dagli script.
|
|
|
|
Le pagine sono deliverable, non sorgenti di verita': ogni numero che mostrano viene da uno script
|
|
Python committato, e qui viene INIETTATO, mai trascritto a mano. La regola nasce da un errore di
|
|
questa sessione — due anni di dati di un grafico scritti a memoria perche' l'output del terminale
|
|
era troncato dal `tail` — e il modo di non ripeterlo e' non avere il passaggio manuale.
|
|
|
|
# 1. produrre i dati
|
|
uv run python scripts/research/r0807_growth_yearly.py --dep 800 --json /tmp/growth800.json
|
|
uv run python scripts/research/r0807_asset_compare.py --json /tmp/compare.json
|
|
uv run python scripts/research/r0807_best_strategy.py --anni 12 --json /tmp/best.json
|
|
|
|
# 2. assemblare
|
|
uv run python scripts/web/build.py --out /tmp/pagine \\
|
|
--dati simulatore=/tmp/series.json confronto=/tmp/compare.json scelta=/tmp/best.json
|
|
|
|
# 3. verificare che le pagine ESEGUANO (node --check valida solo la sintassi)
|
|
node scripts/web/smoke.js /tmp/pagine/*.html
|
|
|
|
`pagina_crescita.html` ha i dati gia' dentro (due configurazioni di versamento a confronto) e non
|
|
richiede iniezione: se manca il segnaposto, il file viene copiato tale e quale.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
QUI = Path(__file__).resolve().parent
|
|
SEGNAPOSTO = "/*__DATA__*/"
|
|
MOTORE = "/*__ENGINE__*/"
|
|
SERIE = "/*__SERIES__*/"
|
|
|
|
|
|
def costruisci(shell: Path, dati: Path | None, out: Path) -> str:
|
|
testo = shell.read_text()
|
|
note = []
|
|
|
|
if MOTORE in testo: # la pagina interattiva porta dentro il motore
|
|
eng = (QUI / "engine.js").read_text()
|
|
eng = eng.split('if (typeof module !== "undefined")')[0].rstrip()
|
|
testo = testo.replace(MOTORE, eng)
|
|
note.append("motore")
|
|
|
|
for tag in (SERIE, SEGNAPOSTO):
|
|
if tag in testo:
|
|
if dati is None:
|
|
raise SystemExit(f"{shell.name}: serve --dati, il file contiene {tag}")
|
|
testo = testo.replace(tag, dati.read_text().strip())
|
|
note.append(dati.name)
|
|
|
|
out.write_text(testo)
|
|
return ", ".join(note) if note else "nessuna iniezione (gia' completa)"
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--out", required=True, help="cartella di destinazione")
|
|
ap.add_argument("--dati", nargs="*", default=[], metavar="nome=percorso.json")
|
|
a = ap.parse_args()
|
|
|
|
mappa = {}
|
|
for voce in a.dati:
|
|
nome, _, perc = voce.partition("=")
|
|
p = Path(perc)
|
|
if not p.exists():
|
|
raise SystemExit(f"dati mancanti per «{nome}»: {p}")
|
|
mappa[nome] = p
|
|
|
|
dest = Path(a.out)
|
|
dest.mkdir(parents=True, exist_ok=True)
|
|
for shell in sorted(QUI.glob("pagina_*.html")):
|
|
nome = shell.stem.replace("pagina_", "")
|
|
out = dest / f"{nome}.html"
|
|
try:
|
|
esito = costruisci(shell, mappa.get(nome), out)
|
|
except SystemExit as e:
|
|
print(f" {nome:12} SALTATA — {e}")
|
|
continue
|
|
print(f" {nome:12} -> {out} ({esito}, {out.stat().st_size/1024:.0f} KB)")
|
|
|
|
for extra in ("engine.js", "smoke.js", "test_engine.js"):
|
|
shutil.copy(QUI / extra, dest / extra)
|
|
print(f"\n verifica: node {dest}/smoke.js {dest}/*.html")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|