Skip to content

Measure card text against the real faces instead of a guess - #9

Merged
HarperZ9 merged 2 commits into
mainfrom
fix/card-width-from-faces
Sep 3, 2026
Merged

Measure card text against the real faces instead of a guess#9
HarperZ9 merged 2 commits into
mainfrom
fix/card-width-from-faces

Conversation

@HarperZ9

@HarperZ9 HarperZ9 commented Sep 3, 2026

Copy link
Copy Markdown
Owner

What was wrong

The card gate measured text by calling the renderer's own width table, so the
check and the drawing agreed with each other no matter what that table said.
The table guessed a width from a character's class and handed every lowercase
letter one number. Against the widest face a reader's machine can resolve it
under-measures 86 of the 95 printable ASCII characters, and the card in this
repository drew past the right rule in DejaVu Sans while the suite stayed
green.

How it is fixed

face-metrics.json holds advance widths measured off the font files with
fontTools, rounded up, with the SHA-256 of every face recorded beside them.
The renderer wraps to the per-character maximum across those faces. The gate
loads the same file and rebuilds each measured face on its own, then asks
whether the line fits in that face. The two can now disagree.

A binding check asserts, per character, that the renderer's table sits at or
above every measured advance. Reverting to the class guess fails it on the
first lowercase m, so no fixture string has to carry that regression.

The greedy-wrap hole

A check that only asks whether text was cut misses a single token longer than
the budget. The wrapper is greedy, so it leaves that token alone on its line,
the joined text still equals the source, and the drawing runs off the page
with nothing red. The gate now reads the width of every line the wrapper hands
back, and the control fixture carries a row of that shape.

Verification

Re-rendered, then measured again by a separate script that walks the font
files a second time rather than trusting either table: Hanken Grotesk, Segoe
UI, Arial, DejaVu Sans, Conso, Consolas, Cascadia Mono, DejaVu Sans Mono.
Nothing on the card reaches the rule at x=916 in any of them.

🤖 Generated with Claude Code

HarperZ9 and others added 2 commits September 3, 2026 11:17
The card gate asked repo_card how wide its own text was. Check and drawing
shared a single table, so the two agreed with each other whatever that table
said. The table guessed a width from a character's class and handed every
lowercase letter one number, which under-measures 86 of the 95 printable ASCII
characters against the widest face a reader's machine can resolve. The card
here drew past the right rule in DejaVu Sans under a green suite.

Widths now come from face-metrics.json, measured off the font files with
fontTools and checked in beside the renderer. The renderer wraps to the
per-character maximum across those faces. The gate loads the same file and
rebuilds each measured face on its own, then asks whether the line fits in
that face, so the two can now disagree. A binding check holds the renderer's
table at or above every measured advance, and putting the class guess back
fails on the first lowercase m.

The gate also reads the width of every line the wrapper hands back rather than
only asking whether text was cut. A token longer than the budget got through
before. The wrapper is greedy, so it leaves that token alone on its line, the
joined text still equals the source, and nothing complains while the drawing
runs off the page. The control fixture carries that shape now.

Re-rendered and checked against Hanken Grotesk, Segoe UI, Arial, DejaVu Sans,
Conso, Consolas, Cascadia Mono and DejaVu Sans Mono. Nothing on the card
reaches the rule at x=916 in any of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite asserts the receipt reports exactly the gates it named, in the
order it ran them. Two gates arrived and the expected list did not move, so
the assertion failed on the first new name. A gate appearing or vanishing
unannounced is what that assertion exists to catch, so the list says the new
names rather than the check being loosened.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@HarperZ9
HarperZ9 merged commit 5e73723 into main Sep 3, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant