Skip to content

Commit 8470b42

Browse files
committed
Extend marginalia contracts: text collisions, registration, grammar
Another pass over the figure library found three more bug classes the geometry contracts didn't yet cover. Each becomes a new contract; the existing fixes for the canonical bugs stay in place. Contract 3 — no two text elements overlap (FigureTextCollisionContract). Catches narrow object_boxes whose tag and value compete for horizontal space. itertools-chain failed: "ITER A" (sans, w=30) and "1 · 2" (mono, w=31) couldn't both fit inside a 70px box with the tag-inside layout. Fix: revert that figure to tag_position="above" (the default) with iter A/B spaced 38px apart, canvas h 64→82. Contracts 4a–d — FIGURES, ATTACHMENTS, SCORES stay in sync (FigureRegistrationContract): * every attached slug is scored, * every scored slug is attached, * every attachment points to a figure that exists in FIGURES, * no FIGURES entry is orphan (a figure counts as used if it appears in ATTACHMENTS or anywhere in scripts/build_prototypes.py — the latter catches journey-section figures and banner-layout prototypes that share src/marginalia.py paint code). Contracts 5a–c — grammar conformance (FigureGrammarContract): * every fill/stroke color is INK, INK_SOFT, EMPHASIS, SOFT_FILL, or "none", * every font-family is FONT_SERIF, FONT_MONO, or FONT_SANS, * every stroke-width is W_HAIRLINE (0.6), W_STROKE (1.0), W_EMPHASIS (1.4), or W_GHOST (0.5). All 109 figures already conform; the contract locks the system against future drift. Suite is now 49 tests (was 41). Geometry, palette, fonts, stroke weights, and registration consistency are all asserted on every test run.
1 parent 470a778 commit 8470b42

4 files changed

Lines changed: 128 additions & 9 deletions

File tree

public/prototyping/production-figures-gestalt.html

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

src/asset_manifest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# Generated by scripts/fingerprint_assets.py. Do not edit by hand.
22
ASSET_PATHS = {'SITE_CSS': '/site.be98c8af1bb8.css', 'SYNTAX_JS': '/syntax-highlight.3b6c7f730d46.js', 'EDITOR_JS': '/editor.dd81f5171b14.js'}
3-
HTML_CACHE_VERSION = '667afb62bbc5'
3+
HTML_CACHE_VERSION = '8a4057af1896'

src/marginalia.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -938,11 +938,11 @@ def yield_delegation(c: Canvas) -> None:
938938

939939
def itertools_chain(c: Canvas) -> None:
940940
"""Itertools · chain joins two iterables into one stream without materialising either."""
941-
c.object_box(0, 0, "iter A", "1 · 2", w=70, h=24, tag_position="inside")
942-
c.object_box(0, 34, "iter B", "3 · 4", w=70, h=24, tag_position="inside")
943-
c.closed_arrow(70, 12, 100, 22, emphasis=False)
944-
c.closed_arrow(70, 46, 100, 36, emphasis=False)
945-
c.object_box(102, 16, "chain", "1 · 2 · 3 · 4", w=140, h=28, tag_position="inside")
941+
c.object_box(0, 14, "iter A", "1 · 2", w=70, h=24)
942+
c.object_box(0, 52, "iter B", "3 · 4", w=70, h=24)
943+
c.closed_arrow(70, 26, 100, 36, emphasis=False)
944+
c.closed_arrow(70, 64, 100, 54, emphasis=False)
945+
c.object_box(102, 30, "chain", "1 · 2 · 3 · 4", w=140, h=28)
946946

947947

948948
def assertion_check(c: Canvas) -> None:
@@ -1329,7 +1329,7 @@ def lazy_stream(c: Canvas) -> None:
13291329
"tuple-frozen": (tuple_frozen, 280, 48),
13301330
"value-types": (value_types, 160, 116),
13311331
"yield-delegation": (yield_delegation, 240, 84),
1332-
"itertools-chain": (itertools_chain, 246, 64),
1332+
"itertools-chain": (itertools_chain, 246, 82),
13331333
"assertion-check": (assertion_check, 304, 76),
13341334
"custom-exception-chain": (custom_exception_chain, 220, 90),
13351335
"exception-group-peel": (exception_group_peel, 240, 50),

tests/test_marginalia_geometry.py

Lines changed: 120 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import re
2121
import unittest
2222

23-
from src.marginalia import FIGURES
23+
from src.marginalia import ATTACHMENTS, FIGURES, SCORES
2424
from src.marginalia_grammar import Canvas
2525

2626
ATTR = re.compile(r'([\w-]+)="([^"]+)"')
@@ -209,5 +209,124 @@ def test_no_text_partially_overlaps_a_rect(self):
209209
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
210210

211211

212+
class FigureTextCollisionContract(unittest.TestCase):
213+
"""Contract 3: no two text elements in the same figure overlap.
214+
215+
Two text bounding boxes that overlap render on top of each other
216+
and become unreadable. This catches narrow object_boxes whose tag
217+
and value compete for horizontal space (the itertools-chain bug
218+
where "ITER A" and "1 · 2" couldn't both fit in a 70px box).
219+
"""
220+
221+
def test_no_two_texts_overlap_in_a_figure(self):
222+
failures: list[str] = []
223+
for name, (paint, w, h) in FIGURES.items():
224+
canvas = Canvas(w=w, h=h)
225+
paint(canvas)
226+
texts = []
227+
for kind, attrs, content in parse_parts(canvas.parts):
228+
if kind == "text":
229+
texts.append((element_bbox(kind, attrs, content), content))
230+
for i, (abox, ac) in enumerate(texts):
231+
for j in range(i + 1, len(texts)):
232+
bbox, bc = texts[j]
233+
if overlaps(abox, bbox):
234+
failures.append(f"{name}: texts {ac!r} and {bc!r} overlap at {abox}{bbox}")
235+
break
236+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
237+
238+
239+
class FigureRegistrationContract(unittest.TestCase):
240+
"""Contract 4: FIGURES, ATTACHMENTS, SCORES stay in sync.
241+
242+
Every slug in ATTACHMENTS must be in SCORES, and vice versa.
243+
Every figure name an attachment points to must exist in FIGURES.
244+
Every paint function in FIGURES must be attached to at least one
245+
slug — orphan paint functions accumulate as dead code and silently
246+
fail to ship the design they encode.
247+
"""
248+
249+
def test_every_attached_slug_is_scored(self):
250+
unscored = set(ATTACHMENTS) - set(SCORES)
251+
self.assertEqual(unscored, set(), f"attached but unscored: {sorted(unscored)}")
252+
253+
def test_every_scored_slug_is_attached(self):
254+
unattached = set(SCORES) - set(ATTACHMENTS)
255+
self.assertEqual(unattached, set(), f"scored but unattached: {sorted(unattached)}")
256+
257+
def test_every_attachment_points_to_a_real_figure(self):
258+
names = {name for items in ATTACHMENTS.values() for _, name, _ in items}
259+
orphan_refs = names - set(FIGURES)
260+
self.assertEqual(orphan_refs, set(), f"attachments reference unknown figures: {sorted(orphan_refs)}")
261+
262+
def test_no_unused_figure_paint_functions(self):
263+
# A figure name counts as "used" if it appears in ATTACHMENTS
264+
# (example-page wiring) or anywhere in scripts/build_prototypes.py
265+
# (journey-section figures, banner prototypes, gestalt galleries).
266+
from pathlib import Path
267+
268+
prototype_src = (
269+
Path(__file__).resolve().parents[1] / "scripts" / "build_prototypes.py"
270+
).read_text()
271+
used = {name for items in ATTACHMENTS.values() for _, name, _ in items}
272+
used |= {name for name in FIGURES if f'"{name}"' in prototype_src or f"'{name}'" in prototype_src}
273+
unused = set(FIGURES) - used
274+
self.assertEqual(
275+
unused, set(),
276+
f"figures defined but never rendered (orphan paint functions): {sorted(unused)}",
277+
)
278+
279+
280+
class FigureGrammarContract(unittest.TestCase):
281+
"""Contract 5: every emitted SVG element uses only the locked
282+
palette, font set, and stroke weights from marginalia_grammar.py.
283+
284+
Drift here is how the design system erodes — one cardinal red
285+
here, one Inter font there, and suddenly the library reads as
286+
independently authored.
287+
"""
288+
289+
def test_every_emitted_color_is_from_the_locked_palette(self):
290+
from src.marginalia_grammar import INK, INK_SOFT, EMPHASIS, SOFT_FILL
291+
292+
allowed = {INK, INK_SOFT, EMPHASIS, SOFT_FILL, "none"}
293+
failures: list[str] = []
294+
for name, (paint, w, h) in FIGURES.items():
295+
canvas = Canvas(w=w, h=h)
296+
paint(canvas)
297+
for kind, attrs, _ in parse_parts(canvas.parts):
298+
for key in ("fill", "stroke"):
299+
color = attrs.get(key, "")
300+
if color and color not in allowed:
301+
failures.append(f"{name}: {kind} {key}={color!r}")
302+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
303+
304+
def test_every_emitted_font_is_from_the_locked_set(self):
305+
from src.marginalia_grammar import FONT_SERIF, FONT_MONO, FONT_SANS
306+
307+
allowed = {FONT_SERIF, FONT_MONO, FONT_SANS}
308+
failures: list[str] = []
309+
for name, (paint, w, h) in FIGURES.items():
310+
canvas = Canvas(w=w, h=h)
311+
paint(canvas)
312+
for kind, attrs, _ in parse_parts(canvas.parts):
313+
family = attrs.get("font-family", "")
314+
if family and family not in allowed:
315+
failures.append(f"{name}: {kind} font-family={family!r}")
316+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
317+
318+
def test_every_stroke_width_is_from_the_locked_set(self):
319+
allowed = {"0.6", "1.0", "1.4", "0.5"} # W_HAIRLINE, W_STROKE, W_EMPHASIS, W_GHOST
320+
failures: list[str] = []
321+
for name, (paint, w, h) in FIGURES.items():
322+
canvas = Canvas(w=w, h=h)
323+
paint(canvas)
324+
for kind, attrs, _ in parse_parts(canvas.parts):
325+
weight = attrs.get("stroke-width", "")
326+
if weight and weight not in allowed:
327+
failures.append(f"{name}: {kind} stroke-width={weight!r}")
328+
self.assertEqual(failures, [], "\n " + "\n ".join(failures))
329+
330+
212331
if __name__ == "__main__":
213332
unittest.main()

0 commit comments

Comments
 (0)