def build(runs, gifs, out, title="MVProp planner — connectivity-first audit", interim=False):
"""runs: list of (label, run_dir). gifs: dict label -> [gif paths]. interim=True frames a
mid-training snapshot (recent values, no hard pass/fail)."""
data = []
for label, rd in runs:
hp = os.path.join(rd, "history.json")
hist = json.load(open(hp)) if os.path.exists(hp) else []
data.append((label, rd, hist))
mv = [(l, h) for (l, rd, h) in data if "mvprop" in l.lower() and h]
if interim:
best = max(mv, key=lambda x: _tail_mean(x[1], "coverage_pct"), default=(None, None))
bl, bh = best
note = (f'best mvprop arm so far — <b>{bl}</b>: connectivity_real '
f'<b>{_tail_mean(bh, "connectivity_real"):.3f}</b>, coverage '
f'<b>{_tail_mean(bh, "coverage_pct")*100:.1f}%</b> (peak {_peak(bh, "coverage_pct")*100:.1f}%).'
if bh else 'no data yet.')
banner = (f'<div class="banner"><b>MID-TRAINING SNAPSHOT — not converged.</b> Values are the '
f'mean of the last ~10 logged points (recent), not a final result; arms still climbing. '
f'{note} GIFs land when a run completes and checkpoints.</div>')
else:
worst_conn = min([_tail_mean(h, "connectivity_real") for _, h in mv], default=float("nan"))
ok = worst_conn >= 0.90
banner = (f'<div class="banner {"" if ok else "bad"}"><b>{"PASS" if ok else "CONNECTIVITY FAILURE"} '
f'— connectivity-first.</b> Worst mvprop connectivity_real (λ₂>0.5) across worlds = '
f'<b>{worst_conn:.3f}</b> (target ≥0.90). Coverage graded only on runs whose graph holds.</div>')
# comparison table (recent = mean of last ~10 points; peak = best seen)
rows = ""
for label, rd, hist in data:
if not hist:
rows += f'<tr><td class="l">{label}</td><td colspan="6"><small>no history yet ({rd})</small></td></tr>'
continue
cr = _tail_mean(hist, "connectivity_real"); cp = _tail_mean(hist, "connectivity_pct")
cv = _tail_mean(hist, "coverage_pct"); pv = _peak(hist, "coverage_pct"); dl = _tail_mean(hist, "dual_lambda")
crc = "ok" if cr >= 0.90 else ("warn" if cr >= 0.7 else "bad")
rows += (f'<tr><td class="l">{label}</td>'
f'<td class="{crc}">{cr:.3f}</td><td>{cp:.3f}</td>'
f'<td>{cv*100:.1f}%</td><td>{pv*100:.1f}%</td><td>{dl:.2f}</td><td>{len(hist)}</td></tr>')
table = (f'<table><tr><th class="l">arm</th><th>CONN_real ↑<br><small>λ₂>0.5 (recent)</small></th>'
f'<th>conn<br><small>λ₂>1e-3</small></th><th>cov<br><small>recent</small></th>'
f'<th>cov<br><small>peak</small></th><th>dual λ</th><th>log pts</th></tr>'
f'{rows}</table>')
# charts + gifs per arm
charts = ""
for label, rd, hist in data:
if not hist:
continue
charts += (f'<h2>{label}</h2><div class="chart">{_svg_lines(hist)}'
f'<div class="legend"><b class="c">■</b> connectivity_real '
f'<b class="v">■</b> coverage (x = iterations)</div></div>')
gl = gifs.get(label, [])
if gl:
cells = "".join(f'<div class="cell">{_b64_img(g)}<div class="cap">{os.path.basename(g)}</div></div>'
for g in gl)
charts += f'<div class="grid">{cells}</div>'
html = (f'<!doctype html><html><head><meta charset="utf-8"><title>{title}</title>'
f'<style>{_CSS}</style></head><body><div class="wrap">'
f'<h1>{title}</h1>'
f'<p class="sub">Exact previous architecture (setpool central critic · lagrangian dual · '
f'local_edge_margin · reach reward · λ̂₂ aux · collision-mask · no connectivity hard-mask), '
f'greedy → distilled MVProp planner. One-variable swap.</p>'
f'{banner}<h2>Summary (recent = mean of last ~10 logged points)</h2>{table}{charts}</div></body></html>')
os.makedirs(os.path.dirname(out) or ".", exist_ok=True)
open(out, "w").write(html)
print(f"wrote {out} ({len(html)//1024} KB)", flush=True)
return out