Files
PythagorasGoal/scripts/web/build.py
T
Adriano Dal Pastro ed2532b991 feat(web): simulatore di accumulo nel browser, verificato contro il Python
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>
2026-08-07 18:44:13 +00:00

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()