Skip to content

Linter misses text overflow when the deck's font isn't installed (estimator/renderer font-substitution mismatch) #3

Description

@claudie-everyfolk

Summary

The post-apply linter (and inspect --issues / fix) silently misses real text overflow when the text's font is not installed on the host machine. The overflow estimator measures wrapped-text height with a font substitute that differs from the one LibreOffice uses to render — so the estimator predicts "fits on one line, no overflow" while the render wraps the text to a second line that spills outside the box and collides with neighboring shapes.

This is the exact frame_overflow_bottom defect class the linter exists to catch, so it's a reliability hole in the part of the tool whose whole value is "a flag is always a real defect." Worse, it's environment-dependent: the same deck flags correctly on a machine where the font is installed, and ships clean where it isn't.

Reproduction (from the 2026-06-15 two-world eval)

Deck: the hands-on-deck arm's final.pptx, slide index 14 (0-based; footer reads "slide 15/18"). After a content-edit renamed arm 2 — the old skillarm 2 — the Anthropic pptx skill:

  • Shape s12: TEXT_BOX [4.32, 2.88 2.61 x 0.40 in], JetBrains Mono 10.5pt, text arm 2 — the Anthropic pptx skill, alignment CENTER.
$ deck.py final.pptx fix --slides 14
fix -> out.pptx: 0 fixed, 0 residue          # ← linter sees nothing

$ deck.py final.pptx inspect --issues        # s12 not listed

But the render wraps skill onto a second line that falls below the 0.40"-tall box, overlapping the arrow and the next chip. The apply that introduced it also printed no ⚠ new/worsened geometry issues section. The defect shipped and all three eval judges flagged it.

Minimal synthetic repro (no eval artifacts needed)

from pptx import Presentation
from pptx.util import Inches, Pt
p = Presentation(); s = p.slides.add_slide(p.slide_layouts[6])
tb = s.shapes.add_textbox(Inches(4.32), Inches(2.88), Inches(2.61), Inches(0.40))
tf = tb.text_frame; tf.word_wrap = True
r = tf.paragraphs[0].add_run(); r.text = "arm 2 — the Anthropic pptx skill"
r.font.name = "JetBrains Mono"; r.font.size = Pt(10.5)
p.save("repro.pptx")

On a host without JetBrains Mono installed: deck.py repro.pptx inspect --issues reports no overflow, but deck.py repro.pptx render img/ wraps the text past the box.

Root cause

The overflow estimator and the renderer disagree on font substitution for an uninstalled font.

  1. ShapeWithPosition._estimate_frame_overflow() (scripts/inventory.py:620) measures text width via _wrap_text_line()draw.textlength(line, font=font), where font = load_measure_font(font_name, size).
  2. load_measure_font() (scripts/inventory.py:143) can't find "JetBrains Mono" (not installed, not bundled), so it falls through to _SANS_FALLBACKSArial (inventory.py:156-157). It is not font-class-aware: a monospace font that's missing falls back to a proportional font, never to a monospace metric.
  3. Arial measures arm 2 — the Anthropic pptx skill at ~146px vs a usable width of ~231px — comfortably one line → all_wrapped_lines == 1 → estimated height ≈ one line ≈ fits the 0.40" box → frame_overflow_bottom stays None.
  4. At render time, soffice substitutes its own (wider) fallback for the missing JetBrains Mono, wraps to two lines, and line 2 renders outside the fixed-height box.

So the estimator under-measures width → predicts no wrap → no vertical overflow, while the renderer wraps and overflows. rec_issues() (scripts/deck.py:274) faithfully reports nothing because frame_overflow_bottom is None.

Note: this is fundamentally a horizontal-extent miss surfacing as a missed vertical overflow — the linter has no horizontal-overflow check, and its vertical check is only as accurate as the substitute font's width metrics. (Same root family as the create-eval blog-screenshot crop: geometry that's wrong in a dimension the linter doesn't faithfully model.)

Suggested directions (non-prescriptive — flagging for fix)

  • Make load_measure_font font-class-aware: detect monospace by name (mono, code, consolas, menlo, jetbrains, courier, iosevka, …) and fall back to a monospace metric, not Arial. Closest cheap win.
  • Align estimator substitution with LibreOffice's, or measure against the same fallback soffice would pick, so the two pipelines can't disagree.
  • Low-confidence signal: when measuring with a substituted font (requested font not found on disk), emit a warnings entry on the shape so the agent knows the overflow estimate for that shape is unreliable rather than trusting a silent "clean."
  • Bundling the deck's actual fonts (and pointing both PIL and soffice at them) would make estimate and render agree by construction.

Environment

  • hands-on-deck 3.0.1, commit 34f6c62 (feat/vertical-anchor), macOS, JetBrains Mono not installed.
  • Reproduces on any host missing a deck's declared (proportional-vs-mono-mismatched) font.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions