diff --git a/provider/src/pacta_provider/webdocs.py b/provider/src/pacta_provider/webdocs.py index 7a9f017..634b5a0 100644 --- a/provider/src/pacta_provider/webdocs.py +++ b/provider/src/pacta_provider/webdocs.py @@ -50,6 +50,13 @@ def _leaf_ok(entry: LogEntry) -> bool: ) +def _leaf_short(component: str) -> str: + """Compact display name for a leaf box at small spans.""" + return (component.replace("-ed25519-verified", "") + .replace("ltl-accumulator-verified", "accum") + .replace("fips205-slhdsa-verified", "slh-dsa")) + + def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str, head_label: str = "Ed25519") -> str: """The accumulator, drawn from its real leaves.""" if not entries: @@ -77,12 +84,22 @@ def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str, head ok = _leaf_ok(entry) component = (((entry.leaf.get("attestation") or {}).get("subject")) or {}).get("component", "?") fill, stroke = ("#e2f2e9", "#1e7f4f") if ok else ("#f4f4f6", "#8a93a0") - out.append(f'') - out.append(f'leaf {node_index}') - short = escape(str(component).replace("-ed25519-verified", "")) + # Boxes must FIT the per-leaf span at any tree size (the + # 2026-08-16 lesson: fixed 112px boxes shingled at 19 + # leaves). Rich boxes while they fit, compact ones after. + box_w = min(112.0, span * 0.94) + compact = box_w < 100 + short = escape(_leaf_short(str(component))) label = short if ok else f"{short} ✗" - out.append(f'{label}') - out.append(f'{node.hex()[:10]}…') + if compact: + out.append(f'') + out.append(f'leaf {node_index}') + out.append(f'{label}') + else: + out.append(f'') + out.append(f'leaf {node_index}') + out.append(f'{label}') + out.append(f'{node.hex()[:10]}…') else: is_root = level_index == len(levels) - 1 out.append(f'') @@ -91,11 +108,19 @@ def _svg_tree(entries: list[LogEntry], root_hex: str, signing_backend: str, head for child in (2 * node_index, 2 * node_index + 1): if (level_index - 1, child) in positions: cx, cy = positions[(level_index - 1, child)] - out.append(f'') + leaf_top = 18 if len(entries) > 9 else 22 + out.append(f'') root_x, root_y = positions[(len(levels) - 1, 0)] - out.append(f'') - out.append(f'Signed Tree Head — {escape(head_label)}({root_hex[:12]}…)') - out.append(f'signed by: {escape(signing_backend)} (verify path attested; signing itself not proven)') + # The head box sizes itself to its longest line (the 2026-08-16 + # lesson: a fixed 380px box let a growing caption spill both sides). + title = f"Signed Tree Head — {head_label}({root_hex[:12]}…)" + line2 = f"signed by: {signing_backend}" + line3 = "(verify path attested; signing itself not proven)" + head_w = max(len(title) * 7.0, len(line2) * 5.3, len(line3) * 5.3) + 28 + out.append(f'') + out.append(f'{escape(title)}') + out.append(f'{escape(line2)}') + out.append(f'{escape(line3)}') out.append(f'') out.append("") return "".join(out) diff --git a/tests/test_web_and_witness.py b/tests/test_web_and_witness.py index 27870a5..0c18907 100644 --- a/tests/test_web_and_witness.py +++ b/tests/test_web_and_witness.py @@ -181,3 +181,32 @@ def test_webdocs_source_carries_no_stale_paper_claims(): # first-use glosses the page promised: STH and axiom cones assert "Signed Tree Head (STH)" in text assert "axiom cones (the exact set of assumptions" in text + + +def test_svg_tree_boxes_never_overlap_or_spill(): + # Regression for 2026-08-16: fixed-width leaf boxes shingled once the + # log outgrew the 8-leaf design, and a fixed head box let its caption + # spill. Render the tree at several sizes and assert geometry. + import re + + from pacta_provider.webdocs import _svg_tree + + class _E: + def __init__(self, i): + self.leaf_hash = f"{i:02x}" * 32 + self.leaf = {"attestation": {"subject": {"component": "betrusted-ed25519-verified"}, + "certificates": [{"status": "proven", "axiom_status": "clean"}]}} + + for n in (8, 19, 33): + svg = _svg_tree([_E(i) for i in range(n)], "ab" * 32, "verified-dalek-serial") + rects = [(float(m.group(1)), float(m.group(2)), float(m.group(3))) + for m in re.finditer(r'([^<]+)', svg).group(1) + assert len(title) * 7.0 <= float(head.group(1)), "head title spills"